/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  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.  For additional information regarding
 * copyright in this work, please see the NOTICE file in the top level
 * directory of this distribution.
 */

package org.apache.roller.weblogger.ui.rendering;

import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.roller.weblogger.config.WebloggerConfig;
import org.apache.roller.weblogger.business.WebloggerFactory;
import org.apache.roller.weblogger.pojos.Weblog;


/**
 * Handles rendering requests for Roller pages/feeds by routing to the appropriate Servlet.
 *
 * This request mapper is used to map all weblog specific urls of the form
 * /<weblog handle>/* to the appropriate servlet for handling the actual
 * request.
 *
 * TODO: we should try and make this class easier to extend and build upon
 */
public class WeblogRequestMapper implements RequestMapper {
    
    private static final Log log = LogFactory.getLog(WeblogRequestMapper.class);
    
    private static final String PAGE_SERVLET = "/roller-ui/rendering/page";
    private static final String FEED_SERVLET = "/roller-ui/rendering/feed";
    private static final String RESOURCE_SERVLET = "/roller-ui/rendering/resources";
    private static final String MEDIA_SERVLET = "/roller-ui/rendering/media-resources";
    private static final String SEARCH_SERVLET = "/roller-ui/rendering/search";
    private static final String RSD_SERVLET = "/roller-ui/rendering/rsd";
    
    private static final String COMMENT_SERVLET = "/roller-ui/rendering/comment";
    private static final String TRACKBACK_SERVLET = "/roller-ui/rendering/trackback";
    
    
    // url patterns that are not allowed to be considered weblog handles
    Set<String> restricted = null;
    
    
    public WeblogRequestMapper() {
        
        this.restricted = new HashSet<>();
        
        // build roller restricted list
        String restrictList = 
                WebloggerConfig.getProperty("rendering.weblogMapper.rollerProtectedUrls");
        if(restrictList != null && !restrictList.isBlank()) {
            String[] restrict = restrictList.split(",");
            this.restricted.addAll(Arrays.asList(restrict));
        }
        
        // add user restricted list
        restrictList = 
                WebloggerConfig.getProperty("rendering.weblogMapper.userProtectedUrls");
        if(restrictList != null && !restrictList.isBlank()) {
            String[] restrict = restrictList.split(",");
            this.restricted.addAll(Arrays.asList(restrict));
        }
    }
    
    
    @Override
    public boolean handleRequest(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
        // kinda silly, but we need to keep track of whether or not the url had
        // a trailing slash so that we can act accordingly
        boolean trailingSlash = false;
        
        String weblogHandle = null;
        String weblogLocale = null;
        String weblogRequestContext = null;
        String weblogRequestData = null;
        
        log.debug("evaluating ["+request.getRequestURI()+"]");
        
        // figure out potential weblog handle
        String servlet = request.getRequestURI();
        String pathInfo = null;
                
        if(servlet != null && servlet.trim().length() > 1) {
            
            if(request.getContextPath() != null) {
                servlet = servlet.substring(request.getContextPath().length());
            }
            
            // strip off the leading slash
            servlet = servlet.substring(1);
            
            // strip off trailing slash if needed
            if(servlet.endsWith("/")) {
                servlet = servlet.substring(0, servlet.length() - 1);
                trailingSlash = true;
            }
            
            if(servlet.indexOf('/') != -1) {
                weblogHandle = servlet.substring(0, servlet.indexOf('/'));
                pathInfo = servlet.substring(servlet.indexOf('/')+1);
            } else {
                weblogHandle = servlet;
            }
        }
        
        log.debug("potential weblog handle = "+weblogHandle);
        
        // check if it's a valid weblog handle
        if(restricted.contains(weblogHandle) || !this.isWeblog(weblogHandle)) {
            log.debug("SKIPPED "+weblogHandle);
            return false;
        }

        // is there a special hostname for the specified hostname?
        String multiHostNameURL =  WebloggerConfig.getProperty("weblog.absoluteurl." + weblogHandle);
        if ( multiHostNameURL != null ) {

            // there is, so check that configured hostname matches the one in the request
            URL weblogAbsoluteURL = new URL( multiHostNameURL );
            String headerHost = request.getHeader("Host");
            String configHost = weblogAbsoluteURL.getHost();

            if (headerHost != null && configHost != null && !headerHost.equals(configHost)) {
                log.debug("SKIPPED " + weblogHandle);
                return false;
            }
        }
        
        log.debug("WEBLOG_URL "+request.getServletPath());
        
        // parse the rest of the url and build forward url
        if(pathInfo != null) {
            
            // parse the next portion of the url
            // we expect [locale/]<context>/<extra>/<info>
            String[] urlPath = pathInfo.split("/", 3);
            
            // if we have a locale, deal with it
            if(this.isLocale(urlPath[0])) {
                weblogLocale = urlPath[0];
                
                // no extra path info specified
                if(urlPath.length == 2) {
                    weblogRequestContext = urlPath[1];
                    weblogRequestData = null;
                    
                // request contains extra path info
                } else if(urlPath.length == 3) {
                    weblogRequestContext = urlPath[1];
                    weblogRequestData = urlPath[2];
                }
            
            // otherwise locale is empty
            } else {
                weblogLocale = null;
                weblogRequestContext = urlPath[0];
                
                // last part of request is extra path info
                if(urlPath.length == 2) {
                    weblogRequestData = urlPath[1];
                    
                // if we didn't have a locale then we have split too much
                // so we reassemble the last 2 path elements together
                } else if(urlPath.length == 3) {
                    weblogRequestData = urlPath[1] + "/" + urlPath[2];
                }
            }
            
        }
        
        // special handling for trailing slash issue
        // we need this because by http standards the urls /foo and /foo/ are
        // supposed to be considered different, so we must enforce that
        if(weblogRequestContext == null && !trailingSlash) {
            // this means someone referred to a weblog index page with the 
            // shortest form of url /<weblog> or /<weblog>/<locale> and we need
            // to do a redirect to /<weblog>/ or /<weblog>/<locale>/
            String redirectUrl = "/" + weblogHandle + "/";
            if(request.getQueryString() != null) {
                redirectUrl += "?"+request.getQueryString();
            }
            
            response.sendRedirect(redirectUrl);
            return true;
            
        } else if(weblogRequestContext != null &&
                "tags".equals(weblogRequestContext)) {
            // tags section can have an index page at /<weblog>/tags/ and
            // a tags query at /<weblog>/tags/tag1+tag2, buth that's it
            if((weblogRequestData == null && !trailingSlash) ||
                    (weblogRequestData != null && trailingSlash)) {
                response.sendError(HttpServletResponse.SC_NOT_FOUND);
                return true;
            }
        } else if(weblogRequestContext != null && trailingSlash) {
            // this means that someone has accessed a weblog url and included
            // a trailing slash, like /<weblog>/entry/<anchor>/ which is not
            // supported, so we need to offer up a 404 Not Found
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return true;
        }
        
        // calculate forward url
        String forwardUrl = calculateForwardUrl(request, weblogHandle, weblogLocale,
                weblogRequestContext, weblogRequestData);
        
        // if we don't have a forward url then the request was invalid somehow
        if(forwardUrl == null) {
            return false;
        }
        
        // dispatch to forward url
        log.debug("forwarding to "+forwardUrl);
        RequestDispatcher dispatch = request.getRequestDispatcher(forwardUrl);
        dispatch.forward(request, response);
        
        // we dealt with this request ourselves, so return "true"
        return true;
    }

    
    /**
     * Convenience method for caculating the servlet forward url given a set
     * of information to make the decision with.
     *
     * handle is always assumed valid, all other params may be null.
     */
    private String calculateForwardUrl(HttpServletRequest request,
                                       String handle, String locale,
                                       String context, String data) {
        
        if(log.isDebugEnabled()) {
            log.debug(handle+","+locale+","+context+","+data);
        }
        
        StringBuilder forwardUrl = new StringBuilder(64);
        
        // POST urls, like comment and trackback servlets
        if("POST".equals(request.getMethod())) {
            // posting to permalink, this means comment or trackback
            if(context.equals("entry")) {
                // trackback requests are required to have an "excerpt" param
                if(request.getParameter("excerpt") != null) {
                    
                    forwardUrl.append(TRACKBACK_SERVLET);
                    forwardUrl.append('/');
                    forwardUrl.append(handle);
                    if(locale != null) {
                        forwardUrl.append('/');
                        forwardUrl.append(locale);
                    }
                    forwardUrl.append('/');
                    forwardUrl.append(context);
                    if(data != null) {
                        forwardUrl.append('/');
                        forwardUrl.append(data);
                    }
                    
                // comment requests are required to have a "content" param
                } else if(request.getParameter("content") != null) {
                    
                    forwardUrl.append(COMMENT_SERVLET);
                    forwardUrl.append('/');
                    forwardUrl.append(handle);
                    if(locale != null) {
                        forwardUrl.append('/');
                        forwardUrl.append(locale);
                    }
                    forwardUrl.append('/');
                    forwardUrl.append(context);
                    if(data != null) {
                        forwardUrl.append('/');
                        forwardUrl.append(data);
                    }
                }
                
            } else {
                // someone posting data where they aren't supposed to
                return null;
            }
            
        } else {
            // no context means weblog homepage
            if(context == null) {
                
                forwardUrl.append(PAGE_SERVLET);
                forwardUrl.append('/');
                forwardUrl.append(handle);
                if(locale != null) {
                    forwardUrl.append('/');
                    forwardUrl.append(locale);
                }
                
            } else {
                
                switch (context) {
                    // requests handled by PageServlet
                    case "page":
                    case "entry":
                    case "date":
                    case "category":
                    case "tags":
                        forwardUrl.append(PAGE_SERVLET);
                        forwardUrl.append('/');
                        forwardUrl.append(handle);
                        if(locale != null) {
                            forwardUrl.append('/');
                            forwardUrl.append(locale);
                        }
                        forwardUrl.append('/');
                        forwardUrl.append(context);
                        if(data != null) {
                            forwardUrl.append('/');
                            forwardUrl.append(data);
                        }
                        break;
                        
                    // requests handled by FeedServlet
                    case "feed":
                        forwardUrl.append(FEED_SERVLET);
                        forwardUrl.append('/');
                        forwardUrl.append(handle);
                        if(locale != null) {
                            forwardUrl.append('/');
                            forwardUrl.append(locale);
                        }
                        if(data != null) {
                            forwardUrl.append('/');
                            forwardUrl.append(data);
                        }
                        break;
                        
                    // requests handled by ResourceServlet
                    case "resource":
                        forwardUrl.append(RESOURCE_SERVLET);
                        forwardUrl.append('/');
                        forwardUrl.append(handle);
                        if(data != null) {
                            forwardUrl.append('/');
                            forwardUrl.append(data);
                        }
                        break;
                        
                    // requests handled by MediaResourceServlet
                    case "mediaresource":
                        forwardUrl.append(MEDIA_SERVLET);
                        forwardUrl.append('/');
                        forwardUrl.append(handle);
                        if(data != null) {
                            forwardUrl.append('/');
                            forwardUrl.append(data);
                        }
                        break;
                        
                    // requests handled by SearchServlet
                    case "search":
                        forwardUrl.append(SEARCH_SERVLET);
                        forwardUrl.append('/');
                        forwardUrl.append(handle);
                        if(locale != null) {
                            forwardUrl.append('/');
                            forwardUrl.append(locale);
                        }
                        break;
                        
                    // requests handled by RSDServlet
                    case "rsd":
                        forwardUrl.append(RSD_SERVLET);
                        forwardUrl.append('/');
                        forwardUrl.append(handle);
                        break;
                        
                    // unsupported url
                    default:
                        return null;
                }
            }
        }
        
        if(log.isDebugEnabled()) {
            log.debug("FORWARD_URL "+forwardUrl.toString());
        }
        
        return forwardUrl.toString();
    }
    
    
    /**
     * convenience method which determines if the given string is a valid
     * weblog handle.
     */
    private boolean isWeblog(String potentialHandle) {
        
        log.debug("checking weblog handle "+potentialHandle);
        
        boolean isWeblog = false;
        
        try {
            Weblog weblog = WebloggerFactory.getWeblogger().getWeblogManager()
                    .getWeblogByHandle(potentialHandle);
            
            if(weblog != null) {
                isWeblog = true;
            }
        } catch(Exception ex) {
            // doesn't really matter to us why it's not a valid website
        }
        
        return isWeblog;
    }
    
    
    /**
     * Convenience method which determines if the given string is a valid
     * locale string.
     */
    private boolean isLocale(String potentialLocale) {
        
        boolean isLocale = false;
        
        // we only support 2 or 5 character locale strings, so check that first
        if(potentialLocale != null && 
                (potentialLocale.length() == 2 || potentialLocale.length() == 5)) {
            
            // now make sure that the format is proper ... e.g. "en_US"
            // we are not going to be picky about capitalization
            String[] langCountry = potentialLocale.split("_");
            if(langCountry.length == 1 && 
                    langCountry[0] != null && langCountry[0].length() == 2) {
                isLocale = true;
                
            } else if(langCountry.length == 2 && 
                    langCountry[0] != null && langCountry[0].length() == 2 && 
                    langCountry[1] != null && langCountry[1].length() == 2) {
                
                isLocale = true;
            }
        }
        
        return isLocale;
    }
    
}
