/*
 * @(#)Http.java
 *
 * Summary: base class to Post, Get, Probe to send/receive HTTP messages.
 *
 * Copyright: (c) 1998-2009 Roedy Green, Canadian Mind Products, http://mindprod.com
 *
 * Licence: This software may be copied and used freely for any purpose but military.
 *          http://mindprod.com/contact/nonmil.html
 *
 * Requires: JDK 1.5+
 *
 * Created with: IntelliJ IDEA IDE.
 *
 * Version History:
 *  1.1 2007-07-19 - improved handling of responseCode
 *  1.2 2007-07-27 - use UTF-8 instead of 8859_1.
 *  1.3 2007-08-24 - readStringBlocking, readBytesBlocking, encoding on Get
 *  1.4 2007-09-26 - add TIMEOUT
 *  1.5 2007-12-30 - add alternate get and post methods that take a full URL.
 *  1.6 2008-01-14 - add gzip option on read
 *  1.7 2008-07-25 - add configurable User-Agent, add Base Http class.
 *  1.8 2008-07-27 - handle case where URL given was not HTTP
 *  1.9 2008-08-22 - support accept-charset, accept-encoding and accept-language. Fix bugs in gzip support.
 *  2.0 2009-02-20 - major refactoring. separate setParms and setPostParms. new send method. Post can have both types of parm.
 */
package com.mindprod.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.Locale;

/**
 * base class to Post, Get, Probe to send/receive HTTP messages.
 * <p/>
 * Originally based on work by Jonathan Revusky
 *
 * @author Roedy Green, Canadian Mind Products
 * @version 2.0 2009-02-20 - major refactoring. separate setParms and setPostParms. new send method. Post can have both types of parm.
 * @since 1998
 */
abstract class Http
    {
    // ------------------------------ CONSTANTS ------------------------------

    /**
     * true if want extra debugging output
     */
    static final boolean DEBUGGING = false;

    /**
     * message length to presume when no length given
     */
    static final int DEFAULT_LENGTH = 32 * 1024;

    /**
     * responseCode to give if is no proper one
     */
    static final int DEFAULT_RESPONSE_CODE = -1;

    /**
     * Accept-Charset for header
     */
    private static final String ACCEPT_CHARSET = "iso-8859-1, utf-8, utf-16, *;q=0.1";

    /**
     * Accept-Encoding for header
     */
    private static final String ACCEPT_ENCODING = "gzip, x-gzip, identity, *;q=0";

    /**
     * Accept property for header
     */
    private static final String ACCEPT_PROPERTY = "text/html, image/png, image/jpeg, image/gif, application/x-java-serialized-object, " +
                                                  "text/x-java-source, text/xml, application/xml, text/css, application/x-java-jnlp-file, text/plain, " +
                                                  "application/zip, application/octet-stream, *; q=.2, */*; q=.2";

    /**
     * responseMessage to give if is no proper one. Might mean for example that you tried to use http: on https: URL.
     */
    static final String DEFAULT_RESPONSE_MESSAGE = "no connect";

    // ------------------------------ FIELDS ------------------------------

    /**
     * parameters we send with the command. c.f. PostParms sent in message body with a post
     */
    private String[] parms;

    /**
     * the page containing the URL we pretend to be.
     * By default null, for none.
     */
    private String referer = null;

    /**
     * responseCode in words from most recent post
     */
    String responseMessage;

    /**
     * the browser we pretend to be, by default Firefox 3.5.4
     *
     * @see <a href="http://mindprod.com/jgloss/http.html">details on User-Agent</a>
     */
    private String userAgent = "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.4) Gecko/20091016 Firefox/3.5.4 (.NET CLR 3.5.30729)";

    /**
     * Allow 50 seconds to connect
     */
    private int connectTimeout = 50 * 1000;

    /**
     * Allow 40 seconds for a read to go without progress
     */
    int readTimeout = 40 * 1000;

    /**
     * responseCode from most recent post
     */
    int responseCode;
    // -------------------------- PUBLIC STATIC METHODS --------------------------

    /**
     * encode a set of parms for the command, separated with ? = & = *
     *
     * @param encoding for URLEncoder
     * @param parms    0..n strings to be send as parameter, alternating keyword/value
     *
     * @return all the parms in one string encoded with lead ?
     * @throws java.io.UnsupportedEncodingException
     *          if bad encoding
     */
    public static String encodeParms( String encoding, String... parms ) throws UnsupportedEncodingException
        {
        // for post, will usually have empty list of parms for command.
        if ( parms == null || parms.length == 0 )
            {
            return "";
            }
        assert ( parms.length & 1 ) == 0 : "must have an even number of parms, keyword=value";

        int estLength = 10; // allow a few slots for multibyte chars
        for ( String p : parms )
            {
            estLength += p.length() + 1;
            }
        final StringBuilder sb = new StringBuilder( estLength );
        for ( int i = 0; i < parms.length - 1; i += 2 )
            {
            sb.append( i == 0 ? "?" : "&" );
            sb.append( URLEncoder.encode( parms[ i ], encoding
                    /* encoding */ ) );
            sb.append( '=' );
            sb.append( URLEncoder.encode( parms[ i + 1 ], encoding
                    /* encoding */ ) );
            }
        return sb.toString();
        }

    // -------------------------- PUBLIC INSTANCE  METHODS --------------------------
    /**
     * ges the Referrer ie. the name of a web page this request ostensibly came from.
     *
     * @return referrer e.g "http://mindprod.com/index.html", null for none.
     * @see <a href="http://mindprod.com/jgloss/http.html">details on Referrer</a>
     */
    public String getReferer()
        {
        return referer;
        }

    /**
     * set the Referrer ie. the name of a web page this request ostensibly came from.
     * Note that the word Referrer is spelled incorrectly as Referer  the HTTP spec.
     *
     * @param referer e.g "http://mindprod.com/index.html", null for none.
     *
     * @see <a href="http://mindprod.com/jgloss/http.html">details on Referrer</a>
     */
    public void setReferer( String referer )
        {
        this.referer = referer;
        }

    /**
     * responseCode from most recent post/get
     * Meaning of various codes are described at HttpURLConnection and at http://mindprod.com/jgloss/http.html
     *
     * @return responseCode
     * @see java.net.HttpURLConnection
     */
    public int getResponseCode()
        {
        return responseCode;
        }

    /**
     * responseCode from most recent post/get
     *
     * @return responseCode
     */
    public String getResponseMessage()
        {
        return responseMessage;
        }

    /**
     * override the default connect timeout of 50 seconds
     *
     * @param connectTimeout timeout to connect in ms. Note int, not long.
     */
    public void setConnectTimeout( int connectTimeout )
        {
        this.connectTimeout = connectTimeout;
        }

    /**
     * set the parms that will be send with the initial command
     *
     * @param parms 0..n strings to be send as parameter, alternating keyword/value
     */
    public void setParms( final String... parms )
        {
        assert ( parms.length & 1 ) == 0 : "must have an even number of parms, keyword=value";
        this.parms = parms;
        }

    /**
     * override the default read timeout of 40 seconds
     *
     * @param readTimeout timeout to connect int ms. Note int, not long.
     */
    public void setReadTimeout( int readTimeout )
        {
        this.readTimeout = readTimeout;
        }

    /**
     * override the default User-Agent
     *
     * @param userAgent User-Agent  a browser uses in an HTTP header to identify itself.
     *                  null for no User Agent.  By default you get Firefox.
     *
     * @see <a href="http://mindprod.com/jgloss/http.html">details on User-Agent</a>
     */
    public void setUserAgent( String userAgent )
        {
        this.userAgent = userAgent;
        }

    // --------------------------- CONSTRUCTORS ---------------------------

    /**
     * no public instantiation.  Just a base class.
     */
    Http()
        {
        }

    // -------------------------- OTHER METHODS --------------------------

    /**
     * get the parms for the command encoded, separated with ? = & = *
     *
     * @param encoding for URLEncoder
     *
     * @return all the parms in one string encoded with lead ?
     * @throws java.io.UnsupportedEncodingException
     *          if bad encoding
     */
    String getEncodedParms( String encoding ) throws UnsupportedEncodingException
        {
        return encodeParms( encoding, this.parms );
        }

    /**
     * process the response from the request we sent the server
     *
     * @param encoding Encoding to use to interpret the result.
     * @param urlc     the HttpURLConnection, all ready to go but for the connect.
     *
     * @return content of the response, decompressed, decoded.
     * @throws java.io.IOException if trouble reading the stream.
     */
    String processResponse( String encoding, HttpURLConnection urlc )
            throws IOException
        {
        // send the message.
        urlc.connect();

        // getResponseCode will block until the server responds.
        // save responseCode for later retrieval
        responseCode = urlc.getResponseCode();
        responseMessage = urlc.getResponseMessage();

        // get size of message. -1 means comes in an indeterminate number of chunks.
        int estimatedLength = urlc.getContentLength();
        if ( estimatedLength < 0 )
            {
            // quite common for no length field
            estimatedLength = DEFAULT_LENGTH;
            }
        // InputStream gives us the raw bytes. We must decompress and decode the 8-bit chars..
        // actually a sun.net.www.protocol.http.HttpURLConnection.HttpInputStream
        final InputStream is = urlc.getInputStream();

        final String contentType = urlc.getContentType();
        //  Content-Type: text/html; charset=utf-8
        int place = contentType.lastIndexOf( "charset=" );
        final String revisedEncoding;
        if ( place >= 0 )
            {
            revisedEncoding = contentType.substring( place + "charset=".length() ).trim().toUpperCase();
            }
        else
            {
            revisedEncoding = encoding;
            }

        // content encoding might be null. We don't handle deflate or Unix compress.
        final boolean gzipped = "gzip".equals( urlc.getContentEncoding() )
                                || "x-gzip".equals( urlc.getContentEncoding() );
        // R E A D
        String result = Read.readStringBlocking( is,
                estimatedLength,
                readTimeout,
                gzipped,
                revisedEncoding );

        if ( DEBUGGING )
            {
            System.out.println( "--------------------------------" );
            System.out.println( "ResponseCode:" + responseCode );
            System.out.println( "ResponseMessage:" + responseMessage );
            System.out.println( "ContentType:" + contentType );
            System.out.println( "CharSet:" + revisedEncoding );
            System.out.println( "ContentEncoding:" + urlc.getContentEncoding() );
            System.out.println( "Result:" + ( result == null ? "null" : result.substring( 0, Math.min( result.length(), 300 ) ) ) );
            }
        // C L O S E
        is.close();
        urlc.disconnect();
        return result;
        }

    /**
     * set up the standard properties on the connection
     *
     * @param urlc Connection we are setting up.
     */
    protected void setStandardProperties( URLConnection urlc )
        {
        urlc.setConnectTimeout( connectTimeout );
        urlc.setReadTimeout( readTimeout );

        if ( userAgent != null )
            {
            urlc.setRequestProperty( "User-Agent", userAgent );
            }
        if ( referer != null )
            {
            // note HTTP spells referrer incorrectly.
            urlc.setRequestProperty( "Referer", referer );
            }
        urlc.setRequestProperty( "Accept", ACCEPT_PROPERTY );
        urlc.setRequestProperty( "Accept-Charset", ACCEPT_CHARSET );
        // no deflate, could be added later if we can find code to handle it.
        urlc.setRequestProperty( "Accept-Encoding", ACCEPT_ENCODING );
        // relaxed, prefer English
        final Locale locale = Locale.getDefault();
        // e.g. en_CA,en;;q=0.9
        urlc.setRequestProperty( "Accept-Language", locale.toString() + "," + locale.getLanguage() + ";q=0.9" );
        }
    }