package nxm.ucl.lib; //not needed for non-nxm use

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;

/** HTTP GET/POST request data library with BASIC authentication support.
    NeXtMidas 2.7.0 {@link #setTimeout(int)} will set both open and read timeout.
    Prior to NeXtMidas 2.7.0, the timeout was only on opening the URL connection,
    hence reading data from URL connection would block indefinitely.
    Since NeXtMidas 2.7.2, the default open and read timeout is now 60 seconds
    instead of infinite (0).

    This class requires Java 5 or higher to function since setting the timeouts
    (open &amp; read) and the fixedLengthStreaming mode are new APIs on the
    URLConnection class.
    @author MSZ / Neon Ngo
*/
public class httpgetdata {

   private static String user=null;
   private static String pass=null;
   private Proxy proxy=null;
   private encoder encode;
   private int openTimeout   = 60000; // HTTP connection open timeout in milliseconds, 0 for infinite
   private int readTimeout   = 60000; // HTTP connection read timeout in milliseconds, 0 for infinite
   private boolean DEBUG     = false; // enable/disable debug messages (including timing debug)
   private boolean timing    = false; // enable/disable timing debug
   private long    startTime = 0;     // only used when timing is true
   private boolean printWarn    = true;  // enable/disable printing of all warnings
   private boolean timeoutDebug = false; // enable/disable of warnings related to connect & read timeouts
   private int     code         = -1;    // the most recent HTTP response code (-1 for none)
   private String  reqmethod    = "GET"; // HTTP request method in use
   private String  contentType  = null;
   // constructors
   /** httpgetdata Default Constructor */
   public httpgetdata() { encode = new encoder(); }
   /** httpgetdata Constructor
    *  @param timeoutMillis timeout in milliseconds
    */
   public httpgetdata(int timeoutMillis) {
     encode = new encoder();
     openTimeout = readTimeout = timeoutMillis;
   }
   /** httpgetdata Constructor
    *  @param openTimeoutMillis open timeout in milliseconds
    *  @param readTimeoutMillis read timeout in milliseconds
    */
   public httpgetdata(int openTimeoutMillis, int readTimeoutMillis) {
     encode      = new encoder();
     openTimeout = openTimeoutMillis;
     readTimeout = readTimeoutMillis;
   }

   /** Main driver method for running httpgetdata as an instance
    *  @param args HTTP arguments
    */
   public static void main(String args[]) {
     try {
       if (args.length< 1 || // url is required
           args.length==2 || // user and password are optional, but if entered must include both
           args.length==4) { // proxyhost and proxyport are optional, but if entered must include both
         println("Usage: <url> [user] [password] [proxyhost] [proxyport] [timeout DEF=5000ms] [postdata DEF=null]");
         return;
       }

       String url       = args[0];
       String user      = (args.length>1)? args[1] : null;
       String pass      = (args.length>2)? args[2] : null;
       String proxyhost = (args.length>3)? args[3] : null;
       int    proxyport = (args.length>4)? Integer.parseInt(args[4]) : 80;
       int    timeout   = (args.length>5)? Integer.parseInt(args[5]) : 5000;
       String postdata  = (args.length>6)? args[6] : null;

       httpgetdata hg = new httpgetdata(timeout);
       if (user!=null)      hg.setUserPass(user, pass);
       if (proxyhost!=null) hg.setProxy(proxyhost, proxyport);

       String response = hg.doRequest(url, postdata);
       println("HTTP RESPONSE = \n"+response);
     } catch (Exception e) {
       e.printStackTrace(System.err);
     }
   }

   /** Set username and password for web server BASIC authentication.
    *  @param puser user name
    *  @param ppass user password
    */
   public void setUserPass(String puser,String ppass) {
     user=puser;
     pass=ppass;
     if (DEBUG) println("Setting user:"+puser+" and password");
     if (user==null || pass==null) {
       warning("httpgetdata: Bad Credentials - Null user or Null password");
     } else {
       Authenticator.setDefault(new MyAuthenticator(puser,ppass));
     }
   }

   /** turn on/off debug output
    *  @param dbg true to turn on debuggin output */
   public void setDEBUG (boolean dbg) { DEBUG = dbg; }
   /** is debug output on or off
    *  @return true if debug output is on */
   public boolean isDEBUG () { return DEBUG; }

   /** turn on/off printing of warning messages to console. Since NeXtMidas 2.9.1
    *  @param b true to turn on printing of warning messages to console */
   public void setShowWarn (boolean b) { printWarn = b; }
   /** is printing of warning messages to console enabled? Since NeXtMidas 2.9.1
    *  @return true if printing of warning messages to console is enabled */
   public boolean isShowWarn () { return printWarn; }

   /** turn on/off printing of timeout warning messages to console. Since NeXtMidas 2.9.1
    *  @param b true to turn on printing of timeout warning messages to console */
   public void setTimeoutDebug (boolean b) { timeoutDebug = b; }
   /** is printing of timeout warning messages to console enabled? Since NeXtMidas 2.9.1
    *  @return true if printing of timeout warning messages to console is enabled */
   public boolean isTimeoutDebug () { return timeoutDebug; }

  /** turn on timing debug output */
   public void startTiming () { timing = true; }
   /** turn off timing debug output */
   public void stopTiming ()  { timing = false; }
   /** is timing debug output on or off
    *  @return true if timeing debug is on */
   public boolean isTiming () { return timing; }
   /** Print elapsed time (in nanoseconds) since previous <code>startTime</code>
       with specified prefix message prepended to output.
       @param prefixMessage message to add on as a prefix before the elapsed time
    */
   final void elapsedTime(String prefixMessage) {
     println(prefixMessage+": "+(System.nanoTime()-startTime));
   }

   /** Call me before connecting if you wish to set a timeout (open and read) for this connection.
    *  @param timeoutMillis timeout in milliseconds */
   public void  setTimeout (int timeoutMillis) { openTimeout = readTimeout = timeoutMillis; }
   /** Get the open timeout in milliseconds
    *  @return the open timeout in milliseconds
    */
   public int   getOpenTimeout () { return openTimeout; }
   /** Get the read timeout in milliseconds
    *  @return the read timeout in milliseconds
    */
   public int   getReadTimeout () { return readTimeout; }
   /** Set the open timeout
    *  @param timeout the open timeout in milliseconds
    */
   public void  setOpenTimeout (int timeout) { openTimeout = timeout; }
   /** Set the read timeout
    *  @param timeout the read timeout in milliseconds
    */
   public void  setReadTimeout (int timeout) { readTimeout = timeout; }
   /** Set the HTTP request method to use when not sending post data [DEF=GET]. Since NeXtMidas 3.1.3
    *  @param method HTTP request method to use when not sending post data */
   public void setRequestMethod (String method) { reqmethod = method; }
   /** Get the HTTP request method to use when not sending post data [DEF=GET]. Since NeXtMidas 3.1.3
    *  @return HTTP request method to use when not sending post data */
   public String getRequestMethod () { return reqmethod; }
   /** get the most recent HTTP response code (-1 for none or if no code can be discerned). Since NeXtMidas 2.9.1
    *  @return the most recent HTTP response code */
   public int   getResponseCode () { return code; }
   /** Get the proxy server being used
    *  @return the proxy server being used
    */
   public Proxy getProxy () { return proxy; }
   /** Set proxy server to use for this instance.
       @param phost proxy hostname, null to clear current proxy setting
       @param pport proxy port, 0 or negative to clear current proxy setting
       @since NeXtMidas 2.7.2
    */
   public void setProxy(String phost,int pport) {
     if (phost==null || pport<1) { proxy=null; return; }
     if (DEBUG) println("Setting proxy "+phost+":"+pport);
     try {
       InetSocketAddress isa = new InetSocketAddress(phost,pport);
       proxy = new Proxy(Proxy.Type.HTTP,isa);
     } catch (Exception e) {
       e.printStackTrace();
       warning("httpgetdata: Exception setting proxy="+e);
     }
   }

   /** Submit a HTTP GET request
    *  @param httpurl URL
    *  @return null on error, otherwise HTTP response data for specified request URL
    */
   public String getPage(String httpurl)               { return doRequest(httpurl,null,openTimeout,readTimeout); }
   /** Submit a HTTP GET request.
    *  @param httpurl URL
    *  @return null on error, otherwise HTTP response data for specified request URL
    */
   public String doRequest(String httpurl)             { return doRequest(httpurl,null,openTimeout,readTimeout); }
   /** Submit a HTTP GET or POST (if post is not null) request.
    *  @param httpurl URL
    *  @param post    Data to submit in a HTTP POST request (when not null)
    *  @return null on error, otherwise HTTP response data for specified request URL
    */
   public String doRequest(String httpurl,String post) { return doRequest(httpurl,post,openTimeout,readTimeout); }
   /** Submit a HTTP POST (if post is not null) request.
    *  @param httpurl URL
    *  @param post    Data to submit in a HTTP POST request (when not null)
    *  @return null on error, otherwise HTTP response data for specified request URL
    */
   public String doPost(String httpurl,String post)    { return doRequest(httpurl,post,openTimeout,readTimeout); }
   /* uses specified timeout instead of this.timeout for a HTTP GET/POST request */

   /** Submit a HTTP GET or POST (if post is not null) request using the specified
       timeout instead of this.timeout.
       @param httpurl     URL to submit a HTTP GET or POST request
       @param post        Data to submit in a HTTP POST request (when not null)
       @param openTimeout URL request timeout in milliseconds (when opening connection)
       @param readTimeout URL request timeout in milliseconds (when reading response from server)
       @return null on error, otherwise HTTP response data for specified request URL
       @since NeXtMidas 2.7.0
    */
   public String doRequest(String httpurl,String post, int openTimeout, int readTimeout) {
     try {
       code = -1; // reset last response code
       if (httpurl==null) { warning("Null httpurl to httpdata"); return null; }
       if (DEBUG || timing) { startTime = System.nanoTime(); println("***Starting "+httpurl+" Request: "+startTime); }
       URL url = (new URI(httpurl)).toURL();
       HttpURLConnection hcon; // NTN: this needs to be a local variable (otherwise this method won't be thread safe)
       if (proxy!=null) hcon = (HttpURLConnection)url.openConnection(proxy);
       else hcon = (HttpURLConnection)url.openConnection();

       if (DEBUG || timing) { elapsedTime("  Created HTTP Connection   "); }
       if (hcon==null) {
         warning("Failed to get Page for User="+user+". Cannot open connection for "+httpurl);
         return null;
       }
       if (openTimeout>0) { hcon.setConnectTimeout(openTimeout); }
       if (readTimeout>0) { hcon.setReadTimeout(readTimeout);    }
       if (user!=null) {
         hcon.setRequestProperty("Authorization", "Basic "+encode.encodeBase64(user+":"+pass));
         if (DEBUG || timing) { elapsedTime("  Set HTTP Properties       "); }
       }
       if (this.contentType != null) { // added in 3.9.1
         hcon.setRequestProperty("Content-Type", this.contentType);
       }
       if (post!=null) {
         if (DEBUG || timing) { elapsedTime("  Starting HTTP POST        "); }
         hcon.setRequestMethod("POST");
         hcon.setFixedLengthStreamingMode(post.length());
         hcon.setDoOutput(true);
         try {
           // FYI: the below will cause an implicit connection to the server
           OutputStream posting = hcon.getOutputStream();
           posting.write(post.getBytes());
           posting.flush();
           posting.close();
         } catch (Exception e) {
           warning("Failed to write data for User="+user+". Cannot POST to "+httpurl);
           return null;
         }
         if (DEBUG || timing) { elapsedTime("  End      HTTP POST        "); }
       } else {
         // since 3.3.0: commented out next line to set "Connection: close" request property on HTTP GET request (Bug 2693)
//       hcon.setRequestProperty("Connection", "close"); // since 3.1.3
         hcon.setRequestMethod(reqmethod);
         if (DEBUG || timing) { elapsedTime("  Starting HTTP connect     "); }
         try {
           hcon.connect();
         } catch (SocketTimeoutException sockEx) {
           if (timeoutDebug) println("Failed to get Page (connect timeout) for User="+user+". Cannot connect for "+httpurl+"\t"+sockEx);
           return null;
         } catch (Exception e) {
           warning("Failed to get Page (connect) for User="+user+". Cannot connect for "+httpurl);
           return null;
         }
         if (DEBUG || timing) { elapsedTime("  End      HTTP connect     "); }
       }

       int codeValue;
       if (DEBUG || timing) { elapsedTime("  Starting HTTP ResponseCode"); }
       try {
         code = codeValue = hcon.getResponseCode();
       } catch (Exception e) {
         warning("Failed to get Page (Response code) for User="+user+". Cannot get response code for url="+httpurl+". "+e);
         if (hcon!=null) hcon.disconnect();
         return null;
       }
       if (DEBUG || timing) { elapsedTime("  End      HTTP ResponseCode("+codeValue+")"); }

       if (codeValue != HttpURLConnection.HTTP_ACCEPTED && codeValue != HttpURLConnection.HTTP_OK ) {
         warning("Failed to get Page (Bad code) for User="+user+". Due to "+codeValue+" "+hcon.getResponseMessage()+" for "+httpurl);
       } else {
         try {
           if (DEBUG || timing) { elapsedTime("  Start HTTP Results read   "); }
           BufferedReader in = new BufferedReader(new InputStreamReader(hcon.getInputStream()));
           String inputLine;
           StringBuilder shttpdata = new StringBuilder(1000);
           //MSZ - had to add the \n at the end b/c it was being lost on text type pages
           while ((inputLine = in.readLine()) != null) shttpdata.append(inputLine).append('\n');
           in.close();
           if (DEBUG) { println("Response="+shttpdata); }
           if (DEBUG || timing) { elapsedTime("  End   HTTP Results read   "); }
           if (hcon!=null) hcon.disconnect();
           if (DEBUG || timing) { elapsedTime("***End HTTP"); }
           return (shttpdata.length()<1)? "": shttpdata.toString();
         } catch (SocketTimeoutException sockEx) {
           if (timeoutDebug) println("Failed to read Page (Read timeout) for User="+user
                                 +". Cannot read from "+httpurl+". (code="+codeValue+")");
           hcon.disconnect();
           throw sockEx;
         }
       }
       hcon.disconnect();
       if (DEBUG || timing) { elapsedTime("***End HTTP"); }
     } catch (Exception e) {
       warning("Error:"+e);
       if (DEBUG || printWarn) e.printStackTrace();
       warning("Page="+httpurl);
     }
     return null;
   }

   final void warning(String msg) { if (printWarn) System.err.println(msg); }
   static final void println(String msg) { System.out.println(msg); }

  /** Get the content type
   *  @return the content type
   */
  public String getContentType () {
    return contentType;
  }

  /** Set the content type
   *  @param contentType the contentType to set
   */
  public void setContentType (String contentType) {
    this.contentType = contentType;
  }
}