/*
 * 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.
 */
package org.apache.jmeter.protocol.http.sampler;

import java.io.BufferedInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

import java.net.BindException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;

import org.apache.jmeter.protocol.http.control.AuthManager;
import org.apache.jmeter.protocol.http.control.Authorization;
import org.apache.jmeter.protocol.http.control.CacheManager;
import org.apache.jmeter.protocol.http.control.CookieManager;
import org.apache.jmeter.protocol.http.control.Header;
import org.apache.jmeter.protocol.http.control.HeaderManager;

import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.testelement.property.PropertyIterator;

import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.util.SSLManager;

import org.apache.jorphan.logging.LoggingManager;

import org.apache.log.Logger;

/**
 * A sampler which understands all the parts necessary to read statistics about
 * HTTP requests, including cookies and authentication.
 * 
 */
public class HTTPSampler extends HTTPSamplerBase {
    private static final Logger log = LoggingManager.getLoggerForClass();

	private static final int MAX_CONN_RETRIES = 
		JMeterUtils.getPropDefault("http.java.sampler.retries" // $NON-NLS-1$
				,10); // Maximum connection retries

	static {
		log.info("Maximum connection retries = "+MAX_CONN_RETRIES); // $NON-NLS-1$
	}
	
	private static final byte[] NULL_BA = new byte[0];// can share these

	/** Handles writing of a post request */
    private transient PostWriter postWriter;

	/**
	 * Constructor for the HTTPSampler object.
     * 
     * Consider using HTTPSamplerFactory.newInstance() instead
	 */
	public HTTPSampler() {
	}

	/**
	 * Set request headers in preparation to opening a connection.
	 * 
	 * @param conn
	 *            <code>URLConnection</code> to set headers on
	 * @exception IOException
	 *                if an I/O exception occurs
	 */
	protected void setPostHeaders(URLConnection conn) throws IOException {
		postWriter = new PostWriter();
		postWriter.setHeaders(conn, this);
	}

    private void setPutHeaders(URLConnection conn) throws IOException {
        postWriter = new PutWriter();
        postWriter.setHeaders(conn, this);
    }

	/**
	 * Send POST data from <code>Entry</code> to the open connection.
     * This also handles sending data for PUT requests
	 * 
	 * @param connection
	 *            <code>URLConnection</code> where POST data should be sent
     * @return a String show what was posted. Will not contain actual file upload content
	 * @exception IOException
	 *                if an I/O exception occurs
	 */
	protected String sendPostData(URLConnection connection) throws IOException {
		return postWriter.sendPostData(connection, this);
	}

    private String sendPutData(URLConnection connection) throws IOException {
        return postWriter.sendPostData(connection, this);
    }

	/**
	 * Returns an <code>HttpURLConnection</code> fully ready to attempt
	 * connection. This means it sets the request method (GET or POST), headers,
	 * cookies, and authorization for the URL request.
	 * <p>
	 * The request infos are saved into the sample result if one is provided.
	 * 
	 * @param u
	 *            <code>URL</code> of the URL request
	 * @param method
	 *            GET, POST etc
	 * @param res
	 *            sample result to save request infos to
	 * @return <code>HttpURLConnection</code> ready for .connect
	 * @exception IOException
	 *                if an I/O Exception occurs
	 */
	protected HttpURLConnection setupConnection(URL u, String method, HTTPSampleResult res) throws IOException {
        SSLManager sslmgr = null;
        if (PROTOCOL_HTTPS.equalsIgnoreCase(u.getProtocol())) {
            try {
                sslmgr=SSLManager.getInstance(); // N.B. this needs to be done before opening the connection
            } catch (Exception e) {
                log.warn("Problem creating the SSLManager: ", e);
            }
        }
		
        HttpURLConnection conn = (HttpURLConnection) u.openConnection();
        // Update follow redirects setting just for this connection
        conn.setInstanceFollowRedirects(getAutoRedirects());

        if (PROTOCOL_HTTPS.equalsIgnoreCase(u.getProtocol())) {
			try {
				if (null != sslmgr){
				    sslmgr.setContext(conn); // N.B. must be done after opening connection
				}
			} catch (Exception e) {
				log.warn("Problem setting the SSLManager for the connection: ", e);
			}
		}

		// a well-bahaved browser is supposed to send 'Connection: close'
		// with the last request to an HTTP server. Instead, most browsers
		// leave it to the server to close the connection after their
		// timeout period. Leave it to the JMeter user to decide.
		if (getUseKeepAlive()) {
			conn.setRequestProperty(HEADER_CONNECTION, KEEP_ALIVE);
		} else {
			conn.setRequestProperty(HEADER_CONNECTION, CONNECTION_CLOSE);
		}

		conn.setRequestMethod(method);
		setConnectionHeaders(conn, u, getHeaderManager(), getCacheManager());
		String cookies = setConnectionCookie(conn, u, getCookieManager());

        setConnectionAuthorization(conn, u, getAuthManager());

		if (method.equals(POST)) {
			setPostHeaders(conn);
		} else if (method.equals(PUT)) {
            setPutHeaders(conn);
        }
        
        if (res != null) {
            res.setURL(u);
            res.setHTTPMethod(method);
            res.setRequestHeaders(getConnectionHeaders(conn));
            res.setCookies(cookies);
        }
        
		return conn;
	}

	/**
	 * Reads the response from the URL connection.
	 * 
	 * @param conn
	 *            URL from which to read response
	 * @return response content
	 * @exception IOException
	 *                if an I/O exception occurs
	 */
	protected byte[] readResponse(HttpURLConnection conn, SampleResult res) throws IOException {
		BufferedInputStream in;

        final int contentLength = conn.getContentLength();
        if ((contentLength == 0) 
        	&& JMeterUtils.getPropDefault("httpsampler.obey_contentlength", // $NON-NLS-1$
        	false)) {
            log.info("Content-Length: 0, not reading http-body");
			res.setResponseHeaders(getResponseHeaders(conn));
			return NULL_BA;
		}

        // works OK even if ContentEncoding is null
        boolean gzipped = ENCODING_GZIP.equals(conn.getContentEncoding());
        
		try {
			if (gzipped) {
				in = new BufferedInputStream(new GZIPInputStream(conn.getInputStream()));
			} else {
				in = new BufferedInputStream(conn.getInputStream());
			}
		} catch (IOException e) {
			if (! (e.getCause() instanceof FileNotFoundException))
			{
				log.error("readResponse: "+e.toString());
				Throwable cause = e.getCause();
				if (cause != null){
				    log.error("Cause: "+cause);
				}
			}
			// Normal InputStream is not available
			InputStream errorStream = conn.getErrorStream();
			if (errorStream == null) {
				log.info("Error Response Code: "+conn.getResponseCode()+", Server sent no Errorpage");
				res.setResponseHeaders(getResponseHeaders(conn));
				return NULL_BA;
			}
			
			log.info("Error Response Code: "+conn.getResponseCode());

			if (gzipped) {
				in = new BufferedInputStream(new GZIPInputStream(errorStream));
			} else {
			    in = new BufferedInputStream(errorStream);
			}
		} catch (Exception e) {
			log.error("readResponse: "+e.toString());
			Throwable cause = e.getCause();
			if (cause != null){
			    log.error("Cause: "+cause);
			}
			in = new BufferedInputStream(conn.getErrorStream());
		}
		return readResponse(res, in, contentLength);
	}

	/**
	 * Gets the ResponseHeaders from the URLConnection
	 * 
	 * @param conn
	 *            connection from which the headers are read
	 * @return string containing the headers, one per line
	 */
	protected String getResponseHeaders(HttpURLConnection conn) {
		StringBuffer headerBuf = new StringBuffer();
		headerBuf.append(conn.getHeaderField(0));// Leave header as is
		// headerBuf.append(conn.getHeaderField(0).substring(0, 8));
		// headerBuf.append(" ");
		// headerBuf.append(conn.getResponseCode());
		// headerBuf.append(" ");
		// headerBuf.append(conn.getResponseMessage());
		headerBuf.append("\n"); //$NON-NLS-1$

        String hfk;
		for (int i = 1; (hfk=conn.getHeaderFieldKey(i)) != null; i++) {
            headerBuf.append(hfk);
            headerBuf.append(": "); // $NON-NLS-1$
            headerBuf.append(conn.getHeaderField(i));
            headerBuf.append("\n"); // $NON-NLS-1$
		}
		return headerBuf.toString();
	}

	/**
	 * Extracts all the required cookies for that particular URL request and
	 * sets them in the <code>HttpURLConnection</code> passed in.
	 * 
	 * @param conn
	 *            <code>HttpUrlConnection</code> which represents the URL
	 *            request
	 * @param u
	 *            <code>URL</code> of the URL request
	 * @param cookieManager
	 *            the <code>CookieManager</code> containing all the cookies
	 *            for this <code>UrlConfig</code>
	 */
	private String setConnectionCookie(HttpURLConnection conn, URL u, CookieManager cookieManager) {
		String cookieHeader = null;
		if (cookieManager != null) {
			cookieHeader = cookieManager.getCookieHeaderForURL(u);
			if (cookieHeader != null) {
				conn.setRequestProperty(HEADER_COOKIE, cookieHeader);
			}
		}
		return cookieHeader;
	}

	/**
	 * Extracts all the required headers for that particular URL request and
	 * sets them in the <code>HttpURLConnection</code> passed in
	 * 
	 * @param conn
	 *            <code>HttpUrlConnection</code> which represents the URL
	 *            request
	 * @param u
	 *            <code>URL</code> of the URL request
	 * @param headerManager
	 *            the <code>HeaderManager</code> containing all the cookies
	 *            for this <code>UrlConfig</code>
	 * @param cacheManager the CacheManager (may be null)
	 */
	private void setConnectionHeaders(HttpURLConnection conn, URL u, HeaderManager headerManager, CacheManager cacheManager) {
        // Add all the headers from the HeaderManager
		if (headerManager != null) {
			CollectionProperty headers = headerManager.getHeaders();
			if (headers != null) {
				PropertyIterator i = headers.iterator();
				while (i.hasNext()) {
					Header header = (Header) i.next().getObjectValue();
					String n = header.getName();
					String v = header.getValue();
					conn.addRequestProperty(n, v);
				}
			}
		}
        if (cacheManager != null){
            cacheManager.setHeaders(conn, u);
        }
	}
    
    /**
     * Get all the headers for the <code>HttpURLConnection</code> passed in
     * 
     * @param conn
     *            <code>HttpUrlConnection</code> which represents the URL
     *            request
     * @return the headers as a string
     */
    private String getConnectionHeaders(HttpURLConnection conn) {
        // Get all the request properties, which are the headers set on the connection
        StringBuffer hdrs = new StringBuffer(100);
        Map requestHeaders = conn.getRequestProperties();
        Set headerFields = requestHeaders.entrySet();
        for(Iterator i = headerFields.iterator(); i.hasNext();) {
        	Map.Entry entry = (Map.Entry)i.next();
        	String headerKey=(String) entry.getKey();
            // Exclude the COOKIE header, since cookie is reported separately in the sample
            if(!HEADER_COOKIE.equalsIgnoreCase(headerKey)) {            
            	List values = (List) entry.getValue();// value is a List of Strings
            	for (int j=0;j<values.size();j++){            		
                    hdrs.append(headerKey);
                    hdrs.append(": "); // $NON-NLS-1$                
                    hdrs.append((String) values.get(j));
                    hdrs.append("\n"); // $NON-NLS-1$
            	}
            }
        }
        return hdrs.toString();
    }

	/**
	 * Extracts all the required authorization for that particular URL request
	 * and sets it in the <code>HttpURLConnection</code> passed in.
	 * 
	 * @param conn
	 *            <code>HttpUrlConnection</code> which represents the URL
	 *            request
	 * @param u
	 *            <code>URL</code> of the URL request
	 * @param authManager
	 *            the <code>AuthManager</code> containing all the cookies for
	 *            this <code>UrlConfig</code>
	 */
	private void setConnectionAuthorization(HttpURLConnection conn, URL u, AuthManager authManager) {
		if (authManager != null) {
			Authorization auth = authManager.getAuthForURL(u);
			if (auth != null) {
				conn.setRequestProperty(HEADER_AUTHORIZATION, auth.toBasicHeader());
			}
		}
	}

	/**
	 * Samples the URL passed in and stores the result in
	 * <code>HTTPSampleResult</code>, following redirects and downloading
	 * page resources as appropriate.
	 * <p>
	 * When getting a redirect target, redirects are not followed and resources
	 * are not downloaded. The caller will take care of this.
	 * 
	 * @param url
	 *            URL to sample
	 * @param method
	 *            HTTP method: GET, POST,...
	 * @param areFollowingRedirect
	 *            whether we're getting a redirect target
	 * @param frameDepth
	 *            Depth of this target in the frame structure. Used only to
	 *            prevent infinite recursion.
	 * @return results of the sampling
	 */
	protected HTTPSampleResult sample(URL url, String method, boolean areFollowingRedirect, int frameDepth) {
		HttpURLConnection conn = null;

		String urlStr = url.toString();
		log.debug("Start : sample " + urlStr);

		HTTPSampleResult res = new HTTPSampleResult();
		res.setMonitor(isMonitor());
        
		res.setSampleLabel(urlStr);
		res.sampleStart(); // Count the retries as well in the time
		try {
			// Sampling proper - establish the connection and read the response:
			// Repeatedly try to connect:
			int retry;
			// Start with 0 so tries at least once, and retries at most MAX_CONN_RETRIES times
			for (retry = 0; retry <= MAX_CONN_RETRIES; retry++) {
				try {
					conn = setupConnection(url, method, res);
					// Attempt the connection:
					conn.connect();
					break;
				} catch (BindException e) {
					if (retry >= MAX_CONN_RETRIES) {
						log.error("Can't connect after "+retry+" retries, "+e);
						throw e;
					}
					log.debug("Bind exception, try again");
					if (conn!=null) {
					    conn.disconnect();
					}
					this.setUseKeepAlive(false);
					continue; // try again
				} catch (IOException e) {
					log.debug("Connection failed, giving up");
					throw e;
				}
			}
			if (retry > MAX_CONN_RETRIES) {
				// This should never happen, but...
				throw new BindException();
			}
			// Nice, we've got a connection. Finish sending the request:
			if (method.equals(POST)) {
				String postBody = sendPostData(conn);
				res.setQueryString(postBody);
            }
            else if (method.equals(PUT)) {
                String putBody = sendPutData(conn);
                res.setQueryString(putBody);
            }
			// Request sent. Now get the response:
			byte[] responseData = readResponse(conn, res);

			res.sampleEnd();
			// Done with the sampling proper.

			// Now collect the results into the HTTPSampleResult:

			res.setResponseData(responseData);

			int errorLevel = conn.getResponseCode();
            String respMsg = conn.getResponseMessage();
    		String hdr=conn.getHeaderField(0);
    		if (hdr == null) {
    		    hdr="(null)";  // $NON-NLS-1$
    		}
            if (errorLevel == -1){// Bug 38902 - sometimes -1 seems to be returned unnecessarily
            	if (respMsg != null) {// Bug 41902 - NPE
	                try {
	                    errorLevel = Integer.parseInt(respMsg.substring(0, 3));
	                    log.warn("ResponseCode==-1; parsed "+respMsg+ " as "+errorLevel);
	                  } catch (NumberFormatException e) {
	                    log.warn("ResponseCode==-1; could not parse "+respMsg+" hdr: "+hdr);
	                  }
            	} else {
            		respMsg=hdr; // for result
                    log.warn("ResponseCode==-1 & null ResponseMessage. Header(0)= "+hdr);
            	}
            }
            if (errorLevel == -1) {
            	res.setResponseCode("(null)"); // $NON-NLS-1$
            } else {
			    res.setResponseCode(Integer.toString(errorLevel));
            }
			res.setSuccessful(isSuccessCode(errorLevel));

			if (respMsg == null) {// has been seen in a redirect
				respMsg=hdr; // use header (if possible) if no message found
			}
			res.setResponseMessage(respMsg);

			String ct = conn.getContentType();
			if (ct != null){
			    res.setContentType(ct);// e.g. text/html; charset=ISO-8859-1
                res.setEncodingAndType(ct);
			}

			res.setResponseHeaders(getResponseHeaders(conn));
			if (res.isRedirect()) {
				res.setRedirectLocation(conn.getHeaderField(HEADER_LOCATION));
			}

            // If we redirected automatically, the URL may have changed
            if (getAutoRedirects()){
                res.setURL(conn.getURL());
            }
            
			// Store any cookies received in the cookie manager:
			saveConnectionCookies(conn, url, getCookieManager());

            // Save cache information
            final CacheManager cacheManager = getCacheManager();
            if (cacheManager != null){
                cacheManager.saveDetails(conn, res);
            }

			res = resultProcessing(areFollowingRedirect, frameDepth, res);

			log.debug("End : sample");
			return res;
		} catch (IOException e) {
			res.sampleEnd();
			// We don't want to continue using this connection, even if KeepAlive is set
            if (conn != null) { // May not exist
            	conn.disconnect();
            }
            conn=null; // Don't process again
			return errorResult(e, res);
		} finally {
			// calling disconnect doesn't close the connection immediately,
			// but indicates we're through with it. The JVM should close
			// it when necessary.
			disconnect(conn); // Disconnect unless using KeepAlive
		}
	}

	protected void disconnect(HttpURLConnection conn) {
		if (conn != null) {
			String connection = conn.getHeaderField(HEADER_CONNECTION);
			String protocol = conn.getHeaderField(0);
			if ((connection == null && (protocol == null || !protocol.startsWith(HTTP_1_1)))
					|| (connection != null && connection.equalsIgnoreCase(CONNECTION_CLOSE))) {
				conn.disconnect();
			}
		}
	}

	/**
	 * From the <code>HttpURLConnection</code>, store all the "set-cookie"
	 * key-pair values in the cookieManager of the <code>UrlConfig</code>.
	 * 
	 * @param conn
	 *            <code>HttpUrlConnection</code> which represents the URL
	 *            request
	 * @param u
	 *            <code>URL</code> of the URL request
	 * @param cookieManager
	 *            the <code>CookieManager</code> containing all the cookies
	 *            for this <code>UrlConfig</code>
	 */
	private void saveConnectionCookies(HttpURLConnection conn, URL u, CookieManager cookieManager) {
		if (cookieManager != null) {
			for (int i = 1; conn.getHeaderFieldKey(i) != null; i++) {
				if (conn.getHeaderFieldKey(i).equalsIgnoreCase(HEADER_SET_COOKIE)) {
					cookieManager.addCookieFromHeader(conn.getHeaderField(i), u);
				}
			}
		}
	}
}
