/*
 * Copyright 2005-2008 Pentaho Corporation.  All rights reserved. 
 * This program is free software; you can redistribute it and/or modify it under the 
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software 
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this 
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html 
 * or from the Free Software Foundation, Inc., 
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; 
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright 2008 - 2009 Pentaho Corporation.  All rights reserved.
 *
 * Created  
 * @author Steven Barkdull
 */

package org.pentaho.pac.server.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.pac.server.i18n.Messages;

/**
 * Provides a thread safe HttpClient with ability to do post and get.
 * 
 * Reuse notes: this class should be easily reusable in other applications. The
 * ONLY dependency on Pentaho code is for localized messages. To minimize coupling
 * (dependencies on other classes), and keep this code reusable by the
 * largest number of clients, this class should never have dependencies on 
 * other pentaho code.
 * 
 * @author Steven Barkdull
 *
 */
public class ThreadSafeHttpClient {
  private static final Log logger = LogFactory.getLog(ThreadSafeHttpClient.class);
  private static final String REQUESTED_MIME_TYPE = "requestedMimeType"; //$NON-NLS-1$
  public static final String DEFAULT_CONSOLE_PROPERTIES_FILE_NAME = "console.properties"; //$NON-NLS-1$
  public static final String CONTENT_CHARACTERSET_PROPERTY = "content.characterset"; //$NON-NLS-1$
  public static final String DEFAULT_CONTENT_CHARACTERSET_VALUE = "utf-8"; //$NON-NLS-1$
  public static String contentCharacterSet = null;
  public enum HttpMethodType {
    POST, GET
  };

  /*
   * @see: http://hc.apache.org/httpclient-3.x/threading.html
   */
  private static final HttpClient CLIENT;
  static {
    MultiThreadedHttpConnectionManager connectionManager = new MultiThreadedHttpConnectionManager();
    CLIENT = new HttpClient(connectionManager);
    CLIENT.getParams().setParameter("http.useragent", ThreadSafeHttpClient.class.getName() ); //$NON-NLS-1$
    loadProperties();
  }

  /**
   * Base Constructor
   */
  public ThreadSafeHttpClient() {
    super();
  }

  /**
   * 
   * @param serviceName
   * @param methodType
   * @param mapParams
   * @return
   * @throws ProxyException if the attempt to communicate with the server fails,
   * if the attempt to read the response from the server fails, if the response
   * stream is unable to be converted into a String.
   */
  public String execRemoteMethod(String baseUrl, String serviceName, HttpMethodType methodType, Map<String, Object> mapParams)
      throws ProxyException  {
    return execRemoteMethod(baseUrl, serviceName, methodType, mapParams, "text/xml"); //$NON-NLS-1$
  }

  /**
   * 
   * @param serviceName String can be null or empty string.
   * @param mapParams
   * @param requestedMimeType
   * @return
   * @throws ProxyException  ProxyException if the attempt to communicate with the server fails,
   * if the attempt to read the response from the server fails, if the response
   * stream is unable to be converted into a String.
   */
  public String execRemoteMethod(String baseUrl, String serviceName, HttpMethodType methodType, Map<String, Object> mapParams,
      String requestedMimeType) throws ProxyException  {

    assert null != baseUrl : "baseUrl cannot be null"; //$NON-NLS-1$
    
    String serviceUrl = baseUrl;
    if (!StringUtils.isEmpty(serviceName)) {
      if (!serviceUrl.endsWith("/")) { //$NON-NLS-1$
        serviceUrl = serviceUrl + "/"; //$NON-NLS-1$
      }
      serviceUrl = serviceUrl + serviceName;
    }
    if (null == mapParams) {
      mapParams = new HashMap<String, Object>();
    }
    mapParams.put(REQUESTED_MIME_TYPE, requestedMimeType);
    
    HttpMethodBase method = null;
    switch (methodType) {
      case POST:
        method = new PostMethod(serviceUrl);
        method.getParams().setContentCharset(contentCharacterSet);//$NON-NLS-1$
        setPostMethodParams( (PostMethod)method, mapParams );
        method.setFollowRedirects( false );
        break;
      case GET:
        method = new GetMethod(serviceUrl);
        method.getParams().setContentCharset(contentCharacterSet);      //$NON-NLS-1$  
        setGetMethodParams( (GetMethod)method, mapParams );
        method.setFollowRedirects( true );
        break;
      default:
        throw new RuntimeException( Messages.getErrorString( "ThreadSafeHttpClient.ERROR_0002_INVALID_HTTP_METHOD_TYPE", methodType.toString() ) );  // can never happen //$NON-NLS-1$
    }
    return executeMethod( method );
  }

  /**
   * Execute the <param>method</param>, and return the server's response as a string
   * @param method the HttpMethod specifying the server URL and parameters to be 
   * passed to the server.
   * @return a string containing the server's response
   * 
   * @throws ProxyException if the attempt to communicate with the server fails,
   * if the attempt to read the response from the server fails, if the response
   * stream is unable to be converted into a String.
   */
  private String executeMethod( HttpMethod method ) throws ProxyException{
    InputStream responseStrm = null;
    try {
      int httpStatus = CLIENT.executeMethod(method);
      if (httpStatus != HttpStatus.SC_OK) {
        // If the response comes as unauthorized access we will throw a proxy exception explaining the reason and
        // what needs to be done to correct it
        if(httpStatus == HttpStatus.SC_UNAUTHORIZED) {
          throw new ProxyException(Messages.getErrorString("ThreadSafeHttpClient.ERROR_0003_AUTHORIZATION_FAILED"));
        }
        String status = method.getStatusLine().toString();
        String uri = method.getURI().toString();
        String errorMsg = Messages.getErrorString( "ThreadSafeHttpClient.ERROR_0001_CLIENT_REQUEST_FAILED", //$NON-NLS-1$
            uri, status );
        logger.error( errorMsg );
        throw new ProxyException(status);  // TODO
      }
      responseStrm = method.getResponseBodyAsStream();
      // trim() is necessary because some jsp's put \n\r at the beginning of
      // the returned text, and the xml processor chokes on \n\r at the beginning.
      String response = IOUtils.toString(responseStrm).trim();
      return response;
    } catch (Exception e) {
      throw new ProxyException(e);
    } finally {
      method.releaseConnection();
    }
  }

  private static void setGetMethodParams( GetMethod method, Map<String, Object> mapParams ) {
    NameValuePair[] params = mapToNameValuePair( mapParams );
    method.setQueryString( params );
  }

  private static void setPostMethodParams( PostMethod method, Map<String, Object> mapParams ) {
    for ( Map.Entry<String,Object> entry : mapParams.entrySet() ) {
      Object o = entry.getValue();
      if ( o instanceof String[] ) {
        for ( String s : (String[])o ) {
          method.addParameter( entry.getKey(), s );
        }
      } else {
        method.setParameter( entry.getKey(), (String)o );
      }
    }
  }
  
  private static NameValuePair[] mapToNameValuePair(Map<String, Object> paramMap) {
    NameValuePair[] pairAr = new NameValuePair[paramMap.size()];
    int idx = 0;
    for (Map.Entry<String, Object> me : paramMap.entrySet()) {
      pairAr[idx] = new NameValuePair(me.getKey(), (String)me.getValue());
      idx++;
    }
    return pairAr;
  }
  
  private static void loadProperties() {
    FileInputStream fis = null;
    Properties properties = null;
    try {
      URL url = ClassLoader.getSystemResource(DEFAULT_CONSOLE_PROPERTIES_FILE_NAME);
      fis = new FileInputStream(new File(url.toURI()));
    } catch (Exception e) {
      contentCharacterSet = DEFAULT_CONTENT_CHARACTERSET_VALUE;
    }
    if (null != fis) {
      properties = new Properties();
      try {
        properties.load(fis);
      } catch (IOException e) {
        contentCharacterSet = DEFAULT_CONTENT_CHARACTERSET_VALUE;
      }
    }
    if (properties != null) {
      contentCharacterSet = properties.getProperty(CONTENT_CHARACTERSET_PROPERTY, DEFAULT_CONTENT_CHARACTERSET_VALUE);
    } else {
      contentCharacterSet = DEFAULT_CONTENT_CHARACTERSET_VALUE;
    }
  }
}