/*
 * 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.
 *
 * $Header:$
 */
package org.apache.beehive.netui.pageflow;

import org.apache.beehive.netui.pageflow.internal.InternalConstants;
import org.apache.beehive.netui.pageflow.internal.PageFlowRequestWrapper;
import org.apache.beehive.netui.pageflow.internal.InternalUtils;
import org.apache.beehive.netui.pageflow.internal.PageFlowInitialization;
import org.apache.beehive.netui.pageflow.handler.Handlers;
import org.apache.beehive.netui.pageflow.handler.FlowControllerHandlerContext;
import org.apache.beehive.netui.pageflow.handler.ForwardRedirectHandler;
import org.apache.beehive.netui.util.internal.InternalStringBuilder;
import org.apache.beehive.netui.util.internal.ServletUtils;
import org.apache.beehive.netui.util.logging.Logger;
import org.apache.struts.config.ModuleConfig;
import org.apache.struts.action.RequestProcessor;

import javax.servlet.ServletException;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.io.IOException;
import java.util.Map;

/**
 * ActionServlet that dynamically registers modules based on naming/location conventions for Struts
 * configuration files that are generated by the Page Flow compiler.  These files are located in
 * /WEB-INF/classes/_pageflow, and are named struts-config-<i>module-name</i>.xml.
 * For auto-registration of config files in other locations, the user may specify additional
 * {@link ModuleConfigLocator} classes in /WEB-INF/beehive-netui-config.xml using the
 * <code>&lt;module-config-locators&gt;</code> element. 
 */
public class PageFlowActionServlet extends AutoRegisterActionServlet
{
    private static final Logger _log = Logger.getInstance( PageFlowActionServlet.class );
    
    private Handlers _handlers;
    
    private static final ModuleConfigLocator[] DEFAULT_MODULE_CONFIG_LOCATORS =
            {
                new DefaultModuleConfigLocator(),
                new LegacyModuleConfigLocator()
            };
    
    
    /**
     * Get the base list of ModuleConfigLocators, to specify locations for auto-registered Struts modules.  By default,
     * this ActionServlet auto-registers Struts modules whose configuration files are located at
     * "/WEB-INF/classes/_pageflow/struts-config-<i>&lt;module&gt;</i>".  Overriding this method allows
     * alternate locations to be specified.  When an unrecognized Struts module is requested, each registered
     * ModuleConfigLocator is queried for a possible path to the configuration file for the module.  If the
     * configuration file is found, the module is auto-registered against the file.
     */
    protected ModuleConfigLocator[] getDefaultModuleConfigLocators()
    {
        return DEFAULT_MODULE_CONFIG_LOCATORS;
    }
    
    /**
     * Default ModuleConfigLocator that looks for Struts module configuration files according to the pattern
     * "/WEB-INF/classes/_pageflow/struts-config-<i>&lt;module&gt;</i>".  An instance of this class
     * is registered by default.
     * 
     * @see PageFlowActionServlet#getDefaultModuleConfigLocators
     */ 
    public static class DefaultModuleConfigLocator implements ModuleConfigLocator, Serializable
    {
        public String getModuleConfigPath( String moduleName )
        {
            InternalStringBuilder moduleConfPath = new InternalStringBuilder( getGenDir() );
            moduleConfPath.append( '/' ).append( PageFlowConstants.PAGEFLOW_MODULE_CONFIG_PREFIX );
            
            if ( moduleName.length() > 1 )
            {
                moduleConfPath.append( moduleName.replace( '/', '-' ) );
            }
            
            moduleConfPath.append( PageFlowConstants.PAGEFLOW_MODULE_CONFIG_EXTENSION );
            return moduleConfPath.toString();
        }
        
        protected String getGenDir()
        {
            return PageFlowConstants.PAGEFLOW_MODULE_CONFIG_GEN_DIR;
        }
    }
    
    /**
     * ModuleConfigLocator that looks for legacy Struts module configuration files according to the pattern
     * "/WEB-INF/struts-config-<i>&lt;module&gt;</i>".  An instance of this class is registered by default.
     * 
     * @see PageFlowActionServlet#getDefaultModuleConfigLocators
     */ 
    protected static class LegacyModuleConfigLocator extends DefaultModuleConfigLocator
    {
        protected String getGenDir()
        {
            return InternalConstants.WEBINF_DIR;
        }
    }

    public void init()
            throws ServletException
    {
        /* todo: NetUI 1.1 -- need to perform initialization in one place; the ServletContextListener */
        // Ensure that we get to do our initializations, even if there's no context listener registered in web.xml.
        ServletContext servletContext = getServletContext();
        
        if ( ! PageFlowInitialization.isInit( servletContext ) )
        {
            PageFlowInitialization.performInitializations(servletContext, null);
        }
        
        _handlers = Handlers.get( servletContext );
        
        super.init();
    }

    protected void process( HttpServletRequest request, HttpServletResponse response )
            throws IOException, ServletException
    {
        // If this is a direct request for a shared flow (.jpfs) or a Faces backing bean (.jsfb), return a 404 status.
        // These are not web-addressable.
        String servletPath = InternalUtils.getDecodedServletPath( request );
        if ( servletPath.endsWith( InternalConstants.SHARED_FLOW_EXTENSION ) ||
             servletPath.endsWith( InternalConstants.FACES_BACKING_EXTENSION ) )
        {
            if ( _log.isDebugEnabled() )
            {
                _log.debug( "Attempt to hit restricted URI " + servletPath + "; 404 error returned." );
            }
            
            response.sendError( HttpServletResponse.SC_NOT_FOUND );
            return;
        }
        
        // First, reinitialize the page flow classloader, for reloading when recompile occurs in dev mode.
        FlowControllerHandlerContext handlerContext = new FlowControllerHandlerContext( request, response, null );
        _handlers.getReloadableClassHandler().reloadClasses( handlerContext );
        
        super.process( request, response );
    }

    /**
     * Get the webapp-relative path to the Struts module configration file for a given module path.  By default,
     * this is "/WEB-INF/classes/_pageflow/struts-config-<i>&lt;module&gt;</i>", but alternate
     * locations can be specified by adding {@link ModuleConfigLocator}s.  
     * 
     * @param modulePath the Struts module path.
     * @return a String that is the path to the Struts configuration file, relative to the web application root.
     * @see #getDefaultModuleConfigLocators
     */ 
    public String getModuleConfPath( String modulePath )
    {
        return super.getModuleConfPath( modulePath );
    }
    
    /**
     * Struts keeps track of the action servlet URL pattern (e.g., *.do) so it can construct action
     * URIs.  We want to prevent it from noticing *.jpf so it doesn't use that to construct the URIs.
     * 
     * @exclude
     */ 
    public void addServletMapping( String servletName, String urlPattern )
    {
        if ( ! urlPattern.endsWith( PageFlowConstants.PAGEFLOW_EXTENSION ) )
        {
            super.addServletMapping( servletName, urlPattern );
        }
    }
    
    /**
     * Tell whether the given module can handle the given path.  If this is the root module (path=="") and it's a 
     * Page Flow module, then it shouldn't try to handle any path that has a slash in it -- it only handles local
     * actions.
     */ 
    protected boolean moduleCanHandlePath( ModuleConfig moduleConfig, RequestProcessor rp, String servletPath )
    {
        if ( moduleConfig.getPrefix().equals( "" ) && servletPath.lastIndexOf( '/' ) > 0
             && rp instanceof PageFlowRequestProcessor )
        {
            return false;
        }
        
        return true;
    }
    
    /**
     * Last chance to handle an unhandled action URI.
     * @return <code>true</code> if this method handled it (by forwarding somewhere or writing to the response).
     */ 
    protected boolean processUnhandledAction( HttpServletRequest request, HttpServletResponse response, String uri )
        throws IOException, ServletException
    {
        //
        // First check to see if we're already in a forwarded fallback request.  If so, just bail.
        //
        PageFlowRequestWrapper rw = PageFlowRequestWrapper.get( request );
        if ( rw.getOriginalServletPath() != null ) return false;       
        
        SharedFlowController sharedFlowToTry = null;
        String uriBaseName = ServletUtils.getBaseName( uri );
        int firstDot = uriBaseName.indexOf( '.' );
        int lastDot = uriBaseName.lastIndexOf( '.' );
        
        if ( firstDot != -1 && firstDot != lastDot )
        {
            String sharedFlowName = uriBaseName.substring( 0, firstDot );
            
            try
            {
                RequestContext rc = new RequestContext( request, response );
                Map defaultSharedFlows = FlowControllerFactory.get( getServletContext() ).getDefaultSharedFlows( rc );
                
                if ( defaultSharedFlows != null )
                {
                    sharedFlowToTry = ( SharedFlowController ) defaultSharedFlows.get( sharedFlowName );
                    uriBaseName = uriBaseName.substring( firstDot + 1 );
                }
            }
            catch ( ClassNotFoundException e )
            {
                ServletUtils.throwServletException(e);
            }
            catch ( InstantiationException e )
            {
                ServletUtils.throwServletException(e);
            }
            catch ( IllegalAccessException e )
            {
                ServletUtils.throwServletException(e);
            }
        }
        else
        {
            sharedFlowToTry = FlowControllerFactory.getGlobalApp( request, response, getServletContext() );
        }
        
        //
        // If we couldn't find an appropriate module, try raising the action on the (deprecated) Global.app.
        //
        
        if ( sharedFlowToTry != null )
        {
            InternalStringBuilder sfActionURI = new InternalStringBuilder( sharedFlowToTry.getModulePath() );
            sfActionURI.append( '/' );
            sfActionURI.append( uriBaseName );
            rw.setOriginalServletPath( uri );
            ForwardRedirectHandler frh = _handlers.getForwardRedirectHandler();
            FlowControllerHandlerContext context = new FlowControllerHandlerContext( request, response, null );
            frh.forward( context, sfActionURI.toString() );
            return true;
        }
        
        return false;
    }
}
