package org.apache.tools.ant.taskdefs;

import org.apache.tools.ant.*;
import org.apache.tools.ant.taskdefs.Touch;
import org.apache.tools.ant.types.EnumeratedAttribute;
import java.io.*;
import java.net.*;
import java.util.*;

/**
 * Access a particular URL using the HTTP protocol. This can be done to retrieve
 * a particular file or just to cause some side effect on the server from accessing that URL.
 * Both HTTP GET and POST methods are supported, as are an arbitrary number of
 * request parameters. Request parameters are sent as part of the request payload
 * (for the POST method) or encoded as part of the URL (for the GET method). You may also
 * specify specific HTTP headers to include with the outgoing request.
 * <p>
 * This task is similar to the <code>get</code> task. It supports all the options of <CODE>get</CODE>
 * (verbose reporting, timestamp based fetches and controlling actions on failures) and one
 * additional option, <CODE>method</CODE> which specifies the HTTP method to use (GET or POST).
 * Unlike <code>get</code>, the <code>dest</code> parameter is optional. If it is not
 * specified, the data returned from the server is discarded.
 * <p>
 * Request parameters are specified by using nested <code>&lt;param&gt;</code> tags, like this:
 * <pre>
 *      &lt;param name="foo" value="bar"/&gt;
 *      &lt;param name="another"&gt;value&lt;/param&gt;
 * </pre>
 * 
 * HTTP request headers are specified by using nested <code>&lt;param&gt;</code> tags, like this:
 * <pre>
 *      &lt;header name="Accept-Language" value="en-us"/&gt;
 *      &lt;header name="Accept-Encoding"&gt;compress&lt;/header&gt;
 * </pre>
 * 
 * @author matth@pobox.com
 */
public class HttpReq extends Task
{
  private static final int GET = 1;
  private static final int POST = 2;

  private String source; // required
  private File dest; // optional
  private boolean verbose = false;
  private boolean useTimestamp = false; //off by default
  private boolean ignoreErrors = false;
  private int method = GET;
  private Vector params = new Vector();
  private Vector headers = new Vector();

  /**
   * Does the work.
   *
   * @exception BuildException Thrown in unrecoverable error.
   */
  public void execute() throws BuildException {
    if (source == null)
    {
      throw new BuildException("src attribute is required", location);
    }

    if (dest != null)
    {
      if (dest.exists() && dest.isDirectory())
      {
        throw new BuildException("The specified destination is a directory",
                                 location);
      }

      if (dest.exists() && !dest.canWrite())
      {
        throw new BuildException("Can't write to " + dest.getAbsolutePath(),
                                 location);
      }
    }

    //set up the URL connection
    URL url = buildURL();

    try
    {
      //set the timestamp to the file date.
      if (method == GET)
      {
        log("HTTP-GET: " + url);
      }
      else
      {
        log("HTTP-POST: " + url);
      }

      URLConnection connection = url.openConnection();

      // modify the headers

      connection.setUseCaches(false);

      long timestamp=0;
      boolean hasTimestamp=false;
      if (useTimestamp && dest != null && dest.exists())
      {
        timestamp=dest.lastModified();
        if (verbose)
        {
          Date t=new Date(timestamp);
          log("local file date : "+t.toString());
        }

        hasTimestamp=true;
      }
      if (useTimestamp && hasTimestamp)
      {
        connection.setIfModifiedSince(timestamp);
      }

      Param header;
      for (int i = 0; i < headers.size(); i++)
      {
        header = (Param)headers.get(i);
        connection.setRequestProperty(header.getName(), header.getValue());
      }

      if (method == POST)
      {
        connection.setDoOutput(true);

        // Create the output payload
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream(256);
        PrintWriter out = new PrintWriter(byteStream);

        writePostData(out);

        out.flush();

        // Set content length and type headers
        connection.setRequestProperty("Content-Length", Integer.toString(byteStream.size()));
        connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

        byteStream.writeTo(connection.getOutputStream());
      }
      else
      {
        // Connect to the server
        connection.connect();
      }

      // Check the response

      //    a 304 result (HTTP only)
      if (connection instanceof HttpURLConnection)
      {
        HttpURLConnection httpConnection=(HttpURLConnection)connection;
        if (httpConnection.getResponseCode()==HttpURLConnection.HTTP_NOT_MODIFIED)
        {
          //not modified so no file download. just return instead
          //and trace out something so the user doesn't think that the 
          //download happened when it didnt
          log("Not modified - so not downloaded");
          return; 
        }
      }

      //REVISIT: at this point even non HTTP connections may support the if-modified-since
      //behaviour -we just check the date of the content and skip the write if it is not
      //newer. Some protocols (FTP) dont include dates, of course. 

      FileOutputStream fos = null;
      
      if ( dest != null ) 
      {
        fos = new FileOutputStream(dest);
      }

      InputStream is=null;
      for (int i=0; i< 3 ; i++)
      {
        try
        {
          is = connection.getInputStream();
          break;
        }
        catch (IOException ex)
        {
          log( "Error opening connection " + ex );
        }
      }
      if (is==null)
      {
        log( "Can't get " + url);
        if (ignoreErrors)
          return;
        throw new BuildException( "Can't get " + source + " to " + dest,
                                  location);
      }

      byte[] buffer = new byte[100 * 1024];
      int length;

      while ((length = is.read(buffer)) >= 0)
      {
        if (fos != null)
          fos.write(buffer, 0, length);
        if (verbose) System.out.print(".");
      }
      if (verbose) System.out.println();
      if (fos != null)
        fos.close();
      is.close();

      //if (and only if) the use file time option is set, then the 
      //saved file now has its timestamp set to that of the downloaded file
      if (useTimestamp && dest != null)
      {
        long remoteTimestamp=connection.getLastModified();
        if (verbose)
        {
          Date t=new Date(remoteTimestamp);
          log("last modified = "+t.toString()
              +((remoteTimestamp==0)?" - using current time instead":""));
        }
        if (remoteTimestamp!=0)
          touchFile(dest,remoteTimestamp);
      }
    }
    catch (IOException ioe)
    {
      log("Error getting " + url + " to " + dest);
      if (ignoreErrors)
        return;
      throw new BuildException(ioe, location);
    }
  }

  private void writePostData(PrintWriter out)
  {
    Param param;
    for (int i = 0; i < params.size(); i++)
    {
      if (i > 0)
      {
        out.print('&');
      }
      param = (Param)params.get(i);
      out.print(URLEncoder.encode(param.getName()));
      out.print('=');
      out.print(URLEncoder.encode(param.getValue()));
    }
  }

  private URL buildURL() throws BuildException
  {
    try
    {
      if (method == GET && params.size() > 0)
      {
        StringBuffer buf = new StringBuffer(source);
        buf.append(source.indexOf('?') == -1 ? '?' : '&');
        Param param;
        for (int i = 0; i < params.size(); i++)
        {
          if (i > 0)
          {
            buf.append('&');
          }
          param = (Param)params.get(i);
          buf.append(URLEncoder.encode(param.getName()));
          buf.append('=');
          buf.append(URLEncoder.encode(param.getValue()));
        }
        return new URL(buf.toString());
      }
      else
      {
        return new URL(source);
      }
    }
    catch(MalformedURLException e)
    {
      throw new BuildException("Invalid src URL", location);
    }
  }

  /** 
   * set the timestamp of a named file to a specified time.
   *
   * @param filename
   * @param time in milliseconds since the start of the era
   * @return true if it succeeded. False means that this is a
   * java1.1 system and that file times can not be set
   *@exception BuildException Thrown in unrecoverable error. Likely
   *this comes from file access failures.
   */
  protected boolean touchFile(File file, long timemillis) 
  throws BuildException  {

    if (project.getJavaVersion() != Project.JAVA_1_1)
    {
      Touch touch = (Touch) project.createTask("touch");
      touch.setOwningTarget(target);
      touch.setTaskName(getTaskName());
      touch.setLocation(getLocation());
      touch.setFile(file);
      touch.setMillis(timemillis);
      touch.touch();
      return true;

    }
    else
    {
      return false;
    }
  }

  /**
   * Adds a form / request parameter.
   */
  public void addParam(Param param)
  {
    params.add(param);
  }

  /**
   * Adds an HTTP request header.
   */
  public void addHeader(Param header)
  {
    headers.add(header);
  }

  /**
   * Set the URL.
   *
   * @param u URL for the file.
   */
  public void setSrc(String u)
  {
    this.source = u;
  }

  /**
   * Where to copy the source file.
   *
   * @param dest Path to file.
   */
  public void setDest(File dest)
  {
    this.dest = dest;
  }

  /**
   * Be verbose, if set to "<CODE>true</CODE>".
   *
   * @param v if "true" then be verbose
   */
  public void setVerbose(boolean v)
  {
    verbose = v;
  }

  /**
   * Don't stop if get fails if set to "<CODE>true</CODE>".
   *
   * @param v if "true" then don't report download errors up to ant
   */
  public void setIgnoreErrors(boolean v)
  {
    ignoreErrors = v;
  }

  public void setMethod(HttpMethodType method)
  {
    if ("post".equalsIgnoreCase(method.getValue()))
    {
      this.method = POST;
    }
    else 
    {
      this.method = GET;
    }
  }

  /**
   * Use timestamps, if set to "<CODE>true</CODE>".
   *
   * <p>In this situation, the if-modified-since header is set so that the file is
   * only fetched if it is newer than the local file (or there is no local file)
   * This flag is only valid on HTTP connections, it is ignored in other cases.
   * When the flag is set, the local copy of the downloaded file will also 
   * have its timestamp set to the remote file time. 
   * <br>
   * Note that remote files of date 1/1/1970 (GMT) are treated as 'no timestamp', and
   * web servers often serve files with a timestamp in the future by replacing their timestamp
   * with that of the current time. Also, inter-computer clock differences can cause no end of 
   * grief. 
   * @param v "true" to enable file time fetching
   */
  public void setUseTimestamp(boolean v)
  {
    if (project.getJavaVersion() != Project.JAVA_1_1)
    {
      useTimestamp = v;
    }
  }

  public static class Param
  {
    public String getName()
    {
      return name;
    }

    public void setName(String name)
    {
      this.name = name;
    }

    public String getValue()
    {
      return value;
    }

    public void setValue(String value)
    {
      this.value = value;
    }

    public void addText(String text)
    {
      this.value = text;
    }

    public String toString()
    {
      return name + "->" + value;
    }

    private String name;
    private String value;
  }

  /**
   * Enumerated attribute with the values "get", and "post".
   */
  public static class HttpMethodType extends EnumeratedAttribute {
      public String[] getValues() {
          return new String[] {"get", "post"};
      }
  }
}
