/*
 * Copyright 2010 JBoss Inc
 *
 * Licensed 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.jbpm.process.workitem.rest;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.httpclient.auth.AuthScope;
import org.apache.commons.httpclient.methods.DeleteMethod;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.httpclient.methods.StringRequestEntity;
import org.jbpm.process.workitem.AbstractLogOrThrowWorkItemHandler;
import org.kie.api.runtime.process.WorkItem;
import org.kie.api.runtime.process.WorkItemManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * WorkItemHandler that is capable of interacting with REST service. Supports both types of services
 * secured (that requires authentication) and open (no authentication). Authentication methods currently supported:
 * <ul>
 *  <li>BASIC</li>
 *  <li>FORM BASED</li>
 * </ul>
 * Authentication information can be given on handler initialization and can be overridden via work item parameters.
 * All other configuration options must be given via work item parameters map:
 * <ul>
 *  <li>Url - resource location to be invoked - mandatory</li>
 *  <li>Method - HTTP method that will be executed - defaults to GET</li>
 *  <li>ContentType - data type in case of sending data - mandatory for POST,PUT</li>
 *  <li>Content - actual data to be sent - mandatory for POST,PUT</li>
 *  <li>ConnectTimeout - connection time out - default to 60 seconds</li>
 *  <li>ReadTimeout - read time out - default to 60 seconds</li>
 *  <li>Username - user name for authentication - overrides one given on handler initialization)</li>
 *  <li>Password - password for authentication - overrides one given on handler initialization)</li>
 *  <li>AuthUrl - url that is handling authentication (usually j_security_check url)</li>
 *  <li>HandleResponseErrors - optional parameter that instructs handler to throw errors in case 
 *  of non successful response codes (other than 2XX)</li>
 * </ul>
 */
public class RESTWorkItemHandler extends AbstractLogOrThrowWorkItemHandler {
    
    private static final Logger logger = LoggerFactory.getLogger(RESTWorkItemHandler.class);
	
	private String username;
	private String password;
	private AuthenticationType type;
	private String authUrl;	
	
	/**
	 * Used when no authentication is required
	 */
	public RESTWorkItemHandler() {
	}
	
	/**
	 * Dedicated constructor when BASIC authentication method shall be used
	 * @param username - user name to be used for authentication
	 * @param password - password to be used for authentication
	 */
	public RESTWorkItemHandler(String username, String password) {
		this.username = username;
		this.password = password;
		this.type = AuthenticationType.BASIC;
	}

	/**
	 * Dedicated constructor when FORM BASED authentication method shall be used
	 * @param username - user name to be used for authentication
	 * @param password - password to be used for authentication
	 * @param authUrl
	 */
	public RESTWorkItemHandler(String username, String password, String authUrl) {
		this.username = username;
		this.password = password;
		this.type = AuthenticationType.FORM_BASED;
		this.authUrl = authUrl;
	}

    public String getAuthUrl() {
		return authUrl;
	}

	public void executeWorkItem(WorkItem workItem, WorkItemManager manager) {
		boolean handleException = false;
    	// extract required parameters
        String urlStr = (String) workItem.getParameter("Url");
        String method = (String) workItem.getParameter("Method");
        String handleExceptionStr = (String) workItem.getParameter("HandleResponseErrors");
        if (urlStr == null) {
            throw new IllegalArgumentException("Url is a required parameter");
        }
        if (method == null || method.trim().length() == 0) {
        	method = "GET";
        }
        if (handleExceptionStr != null) {
        	handleException = Boolean.parseBoolean(handleExceptionStr);
        }
        Map<String,Object> params = workItem.getParameters();

        // optional timeout config parameters, defaulted to 60 seconds
        Integer connectTimeout = getParamAsInt(params.get("ConnectTimeout"));
        if (connectTimeout==null) connectTimeout = 60000;
        Integer readTimeout = getParamAsInt(params.get("ReadTimeout"));
        if (readTimeout==null) readTimeout = 60000;

        HttpClient httpclient = new HttpClient();
        httpclient.getHttpConnectionManager().getParams().setConnectionTimeout(connectTimeout);
        httpclient.getHttpConnectionManager().getParams().setSoTimeout(readTimeout);
	        
        HttpMethod theMethod = null;
        if ("GET".equals(method)) {
        	theMethod = new GetMethod(urlStr); 
        } else if ("POST".equals(method)) {
        	theMethod = new PostMethod(urlStr);
        	setBody(theMethod, params);
        } else if ("PUT".equals(method)) {
            theMethod = new PutMethod(urlStr);
            setBody(theMethod, params);
        } else if ("DELETE".equals(method)) {
            theMethod = new DeleteMethod(urlStr);            
        } else {
            throw new IllegalArgumentException("Method " + method + " is not supported");
        }
        doAuthorization(httpclient, theMethod, params);
        try {
        	int responseCode = httpclient.executeMethod(theMethod);
	        Map<String, Object> results = new HashMap<String, Object>();
	        if (responseCode >= 200 && responseCode < 300) {
	        	theMethod.getResponseBody();
	            postProcessResult(theMethod.getResponseBodyAsString(), results);
	            results.put("StatusMsg", "request to endpoint " + urlStr + " successfully completed " + theMethod.getStatusText());
	        } else {
	        	if (handleException) {
	        		handleException(new RESTServiceException(responseCode, theMethod.getResponseBodyAsString(), urlStr));
	        	} else {
		            logger.warn("Unsuccessful response from REST server (status {}, endpoint {}, response {}", 
		                    responseCode, urlStr, theMethod.getResponseBodyAsString());
		            results.put("StatusMsg", "endpoint " + urlStr + " could not be reached: " + theMethod.getResponseBodyAsString());
	        	}
	        }
            results.put("Status", responseCode);
            
	        // notify manager that work item has been completed
	        manager.completeWorkItem(workItem.getId(), results);
    	} catch (Exception e) {
    		handleException(e);
    	} finally {
    		theMethod.releaseConnection();
    	}
    }
	
	protected Integer getParamAsInt(Object param) {
		if (param == null) {
			return null;
		}
		if (param instanceof String && !((String) param).isEmpty()) {
			return Integer.parseInt((String) param);
		} if (param instanceof Number) {
			return ((Number) param).intValue();
		}
		
		return null;
	}
    
	protected void setBody(HttpMethod theMethod, Map<String, Object> params) {
        if (params.containsKey("Content")) {
            try {
                ((EntityEnclosingMethod)theMethod).setRequestEntity(new StringRequestEntity((String)params.get("Content"), (String)params.get("ContentType"), null));
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException("Cannot set body for REST request " + theMethod, e);
            }
        }
    }

    protected void postProcessResult(String result, Map<String, Object> results) {
        results.put("Result", result);
    }

    protected void doAuthorization(HttpClient httpclient, HttpMethod method, Map<String, Object> params) {
    	if (type == null) {
    		return;
    	}
    	String u = (String) params.get("Username");
    	String p = (String) params.get("Password");
    	if (u == null || p == null) {
    		u = this.username;
    		p = this.password;
    	}
        if (u == null) {
        	throw new IllegalArgumentException("Could not find username");
        }
        if (p == null) {
        	throw new IllegalArgumentException("Could not find password");
        }
        if (type == AuthenticationType.BASIC) {
        	
        	httpclient.getState().setCredentials(
    			new AuthScope(AuthScope.ANY_HOST, AuthScope.ANY_PORT, AuthScope.ANY_REALM),
    			new UsernamePasswordCredentials(u, p)
    			
        	);
        	method.setDoAuthentication(true);
        	httpclient.getParams().setAuthenticationPreemptive(true);
        } else if (type == AuthenticationType.FORM_BASED) {
        	String authUrlStr = (String) params.get("AuthUrl");
        	if (authUrlStr == null) {
        		authUrlStr = authUrl;
        	}
        	if (authUrlStr == null) {
                throw new IllegalArgumentException("Could not find authentication url");
            }
        	try {
        		httpclient.executeMethod(method);
        	} catch (IOException e) {
        		throw new RuntimeException("Could not execute request for form-based authentication", e);
            } finally {
            	method.releaseConnection();
            }
            PostMethod authMethod = new PostMethod(authUrlStr);
            NameValuePair[] data = {
        		new NameValuePair("j_username", u),
                new NameValuePair("j_password", p)
    		};
            authMethod.setRequestBody(data);
            try {
                httpclient.executeMethod(authMethod);
            } catch (IOException e) {
        		throw new RuntimeException("Could not initialize form-based authentication", e);
            } finally {
                authMethod.releaseConnection();
            }
        } else {
        	throw new RuntimeException("Unknown AuthenticationType " + type);
        }
    }

    public void abortWorkItem(WorkItem workItem, WorkItemManager manager) {
        // Do nothing, this work item cannot be aborted
    }
    
    public enum AuthenticationType {
    	BASIC,
    	FORM_BASED
    }

}
