/*
 * Copyright (C) 2001-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.cms.dispatcher;

import com.arsdigita.developersupport.DeveloperSupport;
import com.arsdigita.dispatcher.Dispatcher;
import com.arsdigita.dispatcher.DispatcherHelper;
import com.arsdigita.dispatcher.JSPApplicationDispatcher;
import com.arsdigita.dispatcher.RequestContext;
import com.arsdigita.kernel.security.UserContext;
import com.arsdigita.kernel.security.Util;
import com.arsdigita.ui.login.LoginHelper;
import com.arsdigita.util.LockableImpl;
import com.arsdigita.xml.XML;
import org.apache.log4j.Logger;
import org.xml.sax.helpers.DefaultHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;


/**
 * <p> The Service Dispatcher handles common services shared across all
 * CMS instances. This dispatcher is called by the Subsite dispatcher.</p>
 *
 * <p>Here are the steps for a request to
 * <tt>http://yourserver/cms-service/cheese</tt>:</p>
 *
 * <ol>
 *   <li><p>A client sends a request to the web server, which passes it on to
 *   the global ACS dispatcher.</p></li>
 *
 *   <li><p>The global ACS dispatcher examines the first part of the URL,
 *   notices that the Content Center application is mounted at
 *   <tt>/cms-service</tt> and hands the request to the CMS
 *   Service dispatcher.</p></li>
 *
 *   <li><p>The CMS Service dispatcher determines whether a <tt>Page</tt>
 *   has been registered to the URL <tt>/cheese</tt>.</p></li>
 *
 *   <li><p>If no <tt>Page</tt> is registered to the URL <tt>/cheese</tt>,
 *   then the CMS Service dispatcher hands the request to the
 *   {@link com.arsdigita.dispatcher.JSPApplicationDispatcher},
 *   which serves JSPs and static content from the
 *   <tt>/packages/cms/www</tt>.</p></li>
 * </ol>
 *
 * @author Michael Pih (pihman@arsdigita.com)
 * @version $Revision: #12 $ $DateTime: 2004/04/07 16:07:11 $
 */
public class ServiceDispatcher extends LockableImpl implements Dispatcher {

    public static final String versionId = "$Id: //cms/dev/src/com/arsdigita/cms/dispatcher/ServiceDispatcher.java#12 $ by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";


    /**
     * The path of the file that maps resources.
     */
    private final static String MAP_FILE = "WEB-INF/resources/cms-service-map.xml";

    /**
     * Error logging.
     */
    private static Logger s_log =
        Logger.getLogger(ServiceDispatcher.class.getName());

    /**
     * Mapping between a relative URL and the class name of a
     * ResourceHandler.
     */
    private static HashMap s_pageClasses = new HashMap();

    /**
     * Instantiated ResourceHandler cache. This allows for lazy loading.
     */
    private static SimpleCache s_pages = new SimpleCache();


    private Dispatcher m_notFoundHandler;
    private ArrayList m_trailingSlashList = new ArrayList();


    public ServiceDispatcher() {
        super();

        m_trailingSlashList = new ArrayList();
        requireTrailingSlash("");

        setNotFoundDispatcher(JSPApplicationDispatcher.getInstance());

        readFromFile(MAP_FILE);
    }


    /**
     * Handles requests made to the Content Center package.
     *
     * @param request  The HTTP request
     * @param response The HTTP response
     * @param actx     The request context
     */
    public void dispatch(HttpServletRequest request,
                         HttpServletResponse response,
                         RequestContext actx)
        throws IOException, ServletException {

        DeveloperSupport.startStage("ServiceDispatcher.dispatch");

        String url = actx.getRemainingURLPart();
        String originalUrl = actx.getOriginalURL();

        // Empty remaining URL and doesn't end in trailing slash:
        // probably want to redirect.
        if ( m_trailingSlashList.contains(url) && !originalUrl.endsWith("/") ) {
            DispatcherHelper.sendRedirect(response, originalUrl + "/");
            return;
        }

        // Check user access.
        checkUserAccess(request, response, actx);

        ResourceHandler page = getResource(url);
        if ( page != null ) {
            // Serve the page.
            page.init();
            page.dispatch(request, response, actx);
        } else {
            // Fall back on the JSP application dispatcher.
            m_notFoundHandler.dispatch(request, response, actx);
        }

        DeveloperSupport.endStage("ServiceDispatcher.dispatch");
    }

    /**
     * sets the default page to display if no page can be found for the
     * URL in the page map on dispatch.
     */
    public final void setNotFoundDispatcher(Dispatcher d) {
        m_notFoundHandler = d;
    }

    /**
     * Adds a URL to the list of URLs that are required to have trailing
     * slashes.  A request for url will be redirected to url + "/"
     * if the original URL request (what you see in your browser)
     * doesn't include a trailing slash.  This is required for
     * creating virtual directories, so that relative URLs and redirects
     * work.
     */
    public void requireTrailingSlash(String url) {
        m_trailingSlashList.add(url);
    }


    /**
     * Fetch a page based on the URL stub.
     *
     * @param url The URL stub following the site-node URL
     * @return A ResourceHandler or null if none exists.
     * @pre (url != null)
     */
    protected ResourceHandler getResource(String url) throws ServletException {

        // First check the pages cache for existing pages.
        ResourceHandler page = (ResourceHandler) s_pages.get(url);
        if ( page == null ) {

            // Next check if the URL maps to a page class.
            String pageClassName = (String) s_pageClasses.get(url);
            if ( pageClassName != null ) {

                Class pageClass;
                try {
                    pageClass = Class.forName(pageClassName);
                } catch (ClassNotFoundException e) {
                    s_log.error("error fetching class for ResourceHandler", e);
                    throw new ServletException(e);
                }

                // Try and instantiate the page.
                try {
                    page = (ResourceHandler) pageClass.newInstance();
                } catch (InstantiationException e) {
                    s_log.error("error instantiating a ResourceHandler", e);
                    throw new ServletException(e);
                } catch (IllegalAccessException e) {
                    s_log.error("error instantiating a ResourceHandler", e);
                    throw new ServletException(e);
                }

                page.init();
                s_pages.put(url, page);
            }
        }
        return page;
    }

    /**
     * Map a page to a URL.
     *
     * @param url The URL
     * @param className The name of the ResourceHandler class
     * @pre (url != null && className != null)
     */
    protected void addResource(String url, String className) {
        s_pageClasses.put(url, className);
    }

    /**
     * Release the page at the specified URL.
     *
     * @param url The URL
     * @pre (url != null)
     */
    public static void releaseResource(String url) {
        s_pages.remove(url);
    }

    /**
     * Verify that the user is logged in and is able to view the
     * page. Subclasses can override this method if they need to, but
     * should always be sure to call super.checkUserAccess(...)
     *
     * @deprecated subclasses of ResourceHandler should check the
     * permissions directly on the ContentItem being served.  This
     * method currently acts as a no-op
     *
     * @param request  The HTTP request
     * @param response The HTTP response
     * @param actx     The request context
     **/
    protected void checkUserAccess(HttpServletRequest request,
                                   HttpServletResponse response,
                                   RequestContext actx)
        throws ServletException {

        //    User user = KernelHelper.getCurrentUser(request);

        // If the user is not logged in, redirect to the login page.
        // Disabled totally bogus security check
        // This should be done by the resource handler impl itself
        // which can check real permissions
        //if ( user == null ) {
        //  redirectToLoginPage(request, response);
        // Break.
        //}
    }


    /**
     * Redirects the client to the login page, setting the return url to
     * the current request URI.
     *
     * @exception ServletException If there is an exception thrown while
     * trying to redirect, wrap that exception in a ServletException
     **/
    protected void redirectToLoginPage(HttpServletRequest req,
                                       HttpServletResponse resp)
        throws ServletException {
        String url = Util.getSecurityHelper()
            .getLoginURL(req)
            +"?"+UserContext.RETURN_URL_PARAM_NAME
            +"="+UserContext.encodeReturnURL(req);
        try {
            LoginHelper.sendRedirect(req, resp, url);
        } catch (IOException e) {
            s_log.error("IO Exception", e);
            throw new ServletException(e.getMessage(), e);
        }
    }


    /**
     *
     * Initializes URL-to-Page/Dispatcher/Servlet mappings from a file.
     *
     * Format of the file is XML:
     * <pre>
     * &lt;dispatcher-configuration>
     *   &lt;url-mapping
     *     &lt;url>my-page&lt;/url>
     *     OR &lt;page-class>com.arsdigita.Page.class&lt;/page-class>
     *   &lt;url-mapping
     * &lt;/dispatcher-configuration>
     * </pre>
     */
    private final void readFromFile(final String file) {

        if ( isLocked() ) {
            s_log.error(
                        "error in readFromFile: Cannot configure resources more than once.");
            throw new RuntimeException(
                                       "error in readFromFile: Cannot configure resources once the dispatcher is locked.");
        }

        XML.parseResource(file, newParseConfigHandler(s_pageClasses));

        lock();
    }


    /**
     * <b><font color="red">Expirimental</font></b>
     * Returns a SAX event handler object for setting up a MapDispatcher
     * using an XML config file.
     * @param map A map to configure
     * @return a SAX DefaultHandler object for handling SAX events
     * @pre md.m_map != null
     */
    protected DefaultHandler newParseConfigHandler(Map map) {
        return new PageClassConfigHandler(map);
    }


    /**
     *
     * SAX event handler class for parsing configuration file.
     */
    protected static class PageClassConfigHandler extends DefaultHandler {

        private Map m_map;
        private StringBuffer m_buffer;
        private String m_url;
        private String m_className;

        public PageClassConfigHandler(Map map) {
            m_map = map;
            m_buffer = new StringBuffer();
        }

        public void characters(char[] ch, int start, int len) {
            for (int i = 0; i < len; i++) {
                m_buffer.append(ch[start + i]);
            }
        }

        public void endElement(String uri, String localName, String qn) {
            if ( qn.equals("url") ) {
                m_url = m_buffer.toString().trim();
            } else if ( qn.equals("page-class") ) {
                m_className = m_buffer.toString().trim();
            } else if ( qn.equals("url-mapping") ) {
                m_map.put(m_url, m_className);
            }
            m_buffer = new StringBuffer();
        }
    }

}
