/**
 * Copyright (c) 2003-2005, David A. Czarnecki
 * All rights reserved.
 *
 * Portions Copyright (c) 2003-2005 by Mark Lussier
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 *      this list of conditions and the following disclaimer.
 * Redistributions in binary form must reproduce the above copyright notice,
 *      this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
 * Neither the name of the "David A. Czarnecki" and "blojsom" nor the names of
 * its contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 * Products derived from this software may not be called "blojsom",
 * nor may "blojsom" appear in their name, without prior written permission of
 * David A. Czarnecki.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.blojsom.extension.xmlrpc;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.xmlrpc.XmlRpc;
import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.XmlRpcServer;
import org.blojsom.BlojsomException;
import org.blojsom.authorization.AuthorizationProvider;
import org.blojsom.blog.Blog;
import org.blojsom.blog.BlogUser;
import org.blojsom.blog.BlojsomConfigurationException;
import org.blojsom.extension.xmlrpc.handlers.AbstractBlojsomAPIHandler;
import org.blojsom.fetcher.BlojsomFetcherException;
import org.blojsom.servlet.BlojsomBaseServlet;
import org.blojsom.util.BlojsomProperties;
import org.blojsom.util.BlojsomUtils;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Iterator;
import java.util.Properties;
import java.util.Map;
import java.util.HashMap;


/**
 * Blojsom XML-RPC Servlet
 * <p/>
 * This servlet uses the Jakarta XML-RPC Library (http://ws.apache.org/xmlrpc)
 * 
 * @author Mark Lussier
 * @author David Czarnecki
 * @version $Id: BlojsomXMLRPCServlet.java,v 1.20 2005/01/18 03:31:25 czarneckid Exp $
 */
public class BlojsomXMLRPCServlet extends BlojsomBaseServlet implements BlojsomXMLRPCConstants {

    private Log _logger = LogFactory.getLog(BlojsomXMLRPCServlet.class);
    protected AuthorizationProvider _authorizationProvider;
    protected ServletConfig _servletConfig;

    public static final int XMLRPC_DISABLED = 4000;
    public static final String XMLRPC_DISABLED_MESSAGE = "XML-RPC disabled for the requested blog";

    /**
     * Construct a new Blojsom XML-RPC servlet instance
     */
    public BlojsomXMLRPCServlet() {
    }

    /**
     * Configure the authorization provider
     *
     * @throws ServletException If there is an error instantiating and/or initializing the authorization provider
     */
    protected void configureAuthorization() throws ServletException {
        try {
            Class authorizationProviderClass = Class.forName(_blojsomConfiguration.getAuthorizationProvider());
            _authorizationProvider = (AuthorizationProvider) authorizationProviderClass.newInstance();
            _authorizationProvider.init(_servletConfig, _blojsomConfiguration);
        } catch (ClassNotFoundException e) {
            throw new ServletException(e);
        } catch (InstantiationException e) {
            throw new ServletException(e);
        } catch (IllegalAccessException e) {
            throw new ServletException(e);
        } catch (BlojsomConfigurationException e) {
            throw new ServletException(e);
        }
    }

    /**
     * Configure the flavors for the blog which map flavor values like "html" and "rss" to
     * the proper template and content type
     *
     * @param servletConfig Servlet configuration information
     * @param blogUser {@link BlogUser} information
     * @since blojsom 2.22
     */
    protected void configureFlavorsForUser(ServletConfig servletConfig, BlogUser blogUser) throws ServletException {
        String flavorConfiguration = servletConfig.getInitParameter(BLOJSOM_FLAVOR_CONFIGURATION_IP);
        if (BlojsomUtils.checkNullOrBlank(flavorConfiguration)) {
            flavorConfiguration = DEFAULT_FLAVOR_CONFIGURATION_FILE;
        }

        Map flavors = new HashMap();
        Map flavorToTemplateMap = new HashMap();
        Map flavorToContentTypeMap = new HashMap();
        String user = blogUser.getId();

        Properties flavorProperties = new Properties();
        InputStream is = servletConfig.getServletContext().getResourceAsStream(_baseConfigurationDirectory + user + '/' + flavorConfiguration);
        try {
            flavorProperties.load(is);
            is.close();
            _logger.debug("Loaded flavor information for user: " + user);

            Iterator flavorIterator = flavorProperties.keySet().iterator();
            while (flavorIterator.hasNext()) {
                String flavor = (String) flavorIterator.next();
                String[] flavorMapping = BlojsomUtils.parseCommaList(flavorProperties.getProperty(flavor));
                flavors.put(flavor, flavor);
                flavorToTemplateMap.put(flavor, flavorMapping[0]);
                flavorToContentTypeMap.put(flavor, flavorMapping[1]);

            }

            blogUser.setFlavors(flavors);
            blogUser.setFlavorToTemplate(flavorToTemplateMap);
            blogUser.setFlavorToContentType(flavorToContentTypeMap);
        } catch (IOException e) {
            _logger.error(e);
            throw new ServletException(e);
        }
    }

    /**
     * Configure the XML-RPC API Handlers
     *
     * @param httpServletRequest Request
     * @param httpServletResponse Response
     * @param userID User ID
     * @return {@link XmlRpcServer} configured for the given user or <code>null</code> if the configuration failed
     */
    protected XmlRpcServer configureXMLRPCServer(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, String userID) throws ServletException {
        XmlRpcServer xmlRpcServer = new XmlRpcServer();

        String xmlrpcConfigurationFile = _servletConfig.getInitParameter(BLOG_XMLRPC_CONFIGURATION_IP);
        Properties handlerMapProperties = new Properties();
        InputStream is = _servletConfig.getServletContext().getResourceAsStream(xmlrpcConfigurationFile);

        try {
            handlerMapProperties.load(is);
            is.close();

            // Check for the default XML-RPC handler
            String defaultXMLRPCHandler = handlerMapProperties.getProperty(DEFAULT_XMLRPC_HANDLER_KEY);
            handlerMapProperties.remove(DEFAULT_XMLRPC_HANDLER_KEY);

            // Instantiate an XML-RPC server and separate handler instances for the user ID
            BlogUser blogUser = new BlogUser();
            blogUser.setId(userID);

            // Load the user's blog properties
            Properties blogProperties = new BlojsomProperties();
            is = _servletConfig.getServletContext().getResourceAsStream(_baseConfigurationDirectory + userID + '/' + BLOG_DEFAULT_PROPERTIES);
            if (is == null) {
                return null;
            }

            try {
                blogProperties.load(is);
                is.close();
            } catch (IOException e) {
                _logger.error(e);
                throw new BlojsomConfigurationException(e);
            }

            // If a global blog-home directory has been defined, use it for each user
            if (!BlojsomUtils.checkNullOrBlank(_blojsomConfiguration.getGlobalBlogHome()) &&
                    !blogProperties.containsKey(BLOG_HOME_IP)) {
                String usersBlogHome = _blojsomConfiguration.getGlobalBlogHome() + userID + "/";
                File blogHomeDirectory = new File(usersBlogHome);
                if (!blogHomeDirectory.exists()) {
                    _logger.error("Unable to use blog-home directory for user: " + blogHomeDirectory.toString());
                    throw new BlojsomConfigurationException("Unable to use blog-home directory for user: " + blogHomeDirectory.toString());
                }

                blogProperties.setProperty(BLOG_HOME_IP, usersBlogHome);
                _logger.debug("Setting user blog-home directory: " + usersBlogHome);
            }

            Blog blog = null;
            try {
                blog = new Blog(blogProperties);

                // Check to see if we need to dynamically determine blog-base-url and blog-url?
                BlojsomUtils.resolveDynamicBaseAndBlogURL(httpServletRequest, blog, userID);

                blogUser.setBlog(blog);

                _logger.debug("Configured blojsom user: " + blogUser.getId());
            } catch (BlojsomConfigurationException e) {
                _logger.error(e);
                throw new BlojsomConfigurationException(e);
            }

            // Check to see if XML-RPC is enabled for the blog
            if (!blog.getXmlrpcEnabled().booleanValue()) {
                _logger.error(XMLRPC_DISABLED_MESSAGE);
                throw new ServletException(new XmlRpcException(XMLRPC_DISABLED, XMLRPC_DISABLED_MESSAGE));
            }

            // Load the authentication credentials for the user
            _authorizationProvider.loadAuthenticationCredentials(blogUser);

            // Load the flavors
            configureFlavorsForUser(_servletConfig, blogUser);

            // Instantiate and initialize the XML-RPC handlers
            Iterator handlerIterator = handlerMapProperties.keySet().iterator();
            while (handlerIterator.hasNext()) {
                String handlerName = (String) handlerIterator.next();
                String handlerClassName = handlerMapProperties.getProperty(handlerName);
                Class handlerClass = Class.forName(handlerClassName);
                AbstractBlojsomAPIHandler handler = (AbstractBlojsomAPIHandler) handlerClass.newInstance();
                handler.setFetcher(_fetcher);
                handler.setConfiguration(_blojsomConfiguration);
                handler.setBlogUser(blogUser);
                handler.setAuthorizationProvider(_authorizationProvider);
                handler.setServletConfig(_servletConfig);
                handler.setHttpServletRequest(httpServletRequest);
                handler.setHttpServletResponse(httpServletResponse);
                xmlRpcServer.addHandler(handler.getName(), handler);

                if (defaultXMLRPCHandler != null && defaultXMLRPCHandler.equals(handlerName)) {
                    xmlRpcServer.addHandler(DEFAULT_XMLRPC_HANDLER_KEY, handler);
                    _logger.debug("Added default XML-RPC handler: " + handlerClass + " for user: " + userID);
                }

                _logger.debug("Added [" + handler.getName() + "] API Handler : " + handlerClass + " for user: " + userID);
            }

            return xmlRpcServer;
        } catch (InstantiationException e) {
            throw new ServletException(e);
        } catch (IllegalAccessException e) {
            throw new ServletException(e);
        } catch (ClassNotFoundException e) {
            throw new ServletException(e);
        } catch (IOException e) {
            throw new ServletException(e);
        } catch (BlojsomException e) {
            throw new ServletException(e);
        }
    }

    /**
     * Initialize the blojsom XML-RPC servlet
     * 
     * @param servletConfig Servlet configuration information
     * @throws ServletException If there is an error initializing the servlet
     */
    public void init(ServletConfig servletConfig) throws ServletException {
        super.init(servletConfig);
        _servletConfig = servletConfig;

        // Set the default encoding for the XmlRpc classes to UTF-8
        XmlRpc.setEncoding(UTF8);

        configureBlojsom(_servletConfig);
        configureAuthorization();

        _logger.debug("blojsom XML-RPC: All Your Blog Are Belong To Us");
    }

    /**
     * Service an XML-RPC request by passing the request to the proper handler
     * 
     * @param httpServletRequest  Request
     * @param httpServletResponse Response
     * @throws ServletException If there is an error processing the request
     * @throws IOException      If there is an error during I/O
     */
    protected void service(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws
            ServletException, IOException {
        try {
            httpServletRequest.setCharacterEncoding(UTF8);
        } catch (UnsupportedEncodingException e) {
            _logger.error(e);
        }

        // Determine the appropriate user from the URL
        String user = BlojsomUtils.getUserFromPath(httpServletRequest.getPathInfo());
        if (BlojsomUtils.checkNullOrBlank(user) || "/".equals(user)) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND, "Requested user not found: " + user);
            return;
        }

        // Make sure that the user exists in the system
        XmlRpcServer xmlRpcServer = configureXMLRPCServer(httpServletRequest, httpServletResponse, user);
        if (xmlRpcServer == null) {
            httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND, "Requested user not found: " + user);
            return;
        }

        byte[] result = xmlRpcServer.execute(httpServletRequest.getInputStream());
        httpServletResponse.setContentType("text/xml; charset=UTF-8");
        httpServletResponse.setContentLength(result.length);
        OutputStream os = httpServletResponse.getOutputStream();
        os.write(result);
        os.flush();
    }

    /**
     * Called when removing the servlet from the servlet container
     */
    public void destroy() {
        try {
            _fetcher.destroy();
        } catch (BlojsomFetcherException e) {
            _logger.error(e);
        }
    }
}
