Index: Get.java
===================================================================
RCS file: /home/cvspublic/jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Get.java,v
retrieving revision 1.8
diff -u -r1.8 Get.java
--- Get.java	2001/01/03 14:18:30	1.8
+++ Get.java	2001/02/12 15:32:53
@@ -58,21 +58,103 @@
 import java.net.*;
 import java.util.*;
 import org.apache.tools.ant.*;
+import org.apache.tools.ant.types.EnumeratedAttribute;
 
 /**
- * Get a particular file from a URL source. 
- * Options include verbose reporting, timestamp based fetches and controlling 
- * actions on failures. NB: access through a firewall only works if the whole 
- * Java runtime is correctly configured.
- *
+ * 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.
+ * Options include verbose reporting, timestamp based fetches, HTTP GET and POST methods,
+ * and support for Basic authentication. Request parameters can be specified and are sent
+ * as part of the request payload for the POST method or encoded as part of the URL
+ * for GET requests. You may also specify arbitrary HTTP headers to include with the
+ * outgoing request.
+ * <p>
+ * The following parameters are supported:<P>
+ * <TABLE cellSpacing=0 cellPadding=2 border=1>
+ * <TR>
+ * <TD><B>Attribute</B></TD>
+ * <TD><B>Description</B></TD>
+ * <TD align="center"><B>Required</B></TD></TR>
+ * <TR>
+ * <TD>src</TD>
+ * <TD>the URL from which to retrieve a file.</TD>
+ * <TD align="center">Yes</TD></TR>
+ * <TR>
+ * <TD>dest</TD>
+ * <TD>the file where to store the retrieved file.</TD>
+ * <TD align="center">No</TD></TR>
+ * <TR>
+ * <TD>verbose</TD>
+ * <TD>show verbose progress information ("on"/"off").</TD>
+ * <TD align="center">No</TD></TR>
+ * <TR>
+ * <TD>ignoreerrors</TD>
+ * <TD>Log errors but don't treat as fatal.</TD>
+ * <TD align="center">No</TD></TR>
+ * <TR>
+ * <TD>usetimestamp</TD>
+ * <TD>conditionally download a file based on the timestamp of the local copy. HTTP only</TD>
+ * <TD align="center">No</TD>
+ * </TR>
+ * <TR>
+ * <TD>method</TD>
+ * <TD>The HTTP request method used to retrieve the page, "get" or "post".</TD>
+ * <TD align="center">No</TD>
+ * </TR>
+ * <TR>
+ * <TD>authtype</TD>
+ * <TD>Sets the authentication method used when accessing the specified source URL. 
+ *     Currently only "basic" authentication is supported.</TD>
+ * <TD align="center">No</TD>
+ * </TR>
+ * <TR>
+ * <TD>username</TD>
+ * <TD>The username passed for authentication.</TD>
+ * <TD align="center">No</TD>
+ * </TR>
+ * <TR>
+ * <TD>password</TD>
+ * <TD>The password used for authentication.</TD>
+ * <TD align="center">No</TD>
+ * </TR>
+ * </TABLE>
+ * <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>
+ * 
+ * Note: access through a firewall may not work unless the Java runtime is configured
+ * appropriately.
+ * 
  * @author costin@dnt.ro
+ * @author matth@pobox.com
  */
 public class Get extends Task {
-    private URL source; // required
-    private File dest; // required
+    private static final int HTTP_GET = 1;
+    private static final int HTTP_POST = 2;
+
+    private static final int AUTH_NONE = 0;
+    private static final int AUTH_BASIC = 1;
+
+    private String source; // required
+    private File dest;
     private boolean verbose = false;
     private boolean useTimestamp = false; //off by default
     private boolean ignoreErrors = false;
+    private int method = HTTP_GET;
+    private int authType = AUTH_NONE;
+    private String username;
+    private String password;
+    private Vector params = new Vector();
+    private Vector headers = new Vector();
     
     /**
      * Does the work.
@@ -83,118 +165,238 @@
         if (source == null) {
             throw new BuildException("src attribute is required", location);
         }
-
-        if (dest == null) {
-            throw new BuildException("dest attribute is required", location);
-        }
 
-        if (dest.exists() && dest.isDirectory()) { 
+        if (dest != null)
+        {
+          if (dest.exists() && dest.isDirectory())
+          {
             throw new BuildException("The specified destination is a directory",
                                      location);
-        }
+          }
 
-        if (dest.exists() && !dest.canWrite()) { 
+          if (dest.exists() && !dest.canWrite())
+          {
             throw new BuildException("Can't write to " + dest.getAbsolutePath(),
                                      location);
+          }
         }
+
+        //set up the URL connection
+        URL url = buildURL();
 
-        try {
+        try
+        {
+          //set the timestamp to the file date.
+          if (method == HTTP_GET)
+          {
+            log("HTTP-HTTP_GET: " + url);
+          }
+          else
+          {
+            log("HTTP-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());
+            }
 
-            log("Getting: " + source);
+            hasTimestamp=true;
+          }
+          if (useTimestamp && hasTimestamp)
+          {
+            connection.setIfModifiedSince(timestamp);
+          }
+
+          // Set authorization eader, if specified
+          if (authType == AUTH_BASIC && username != null)
+          {
+            password = password == null ? "" : password;
+            String encodeStr = username + ":" + password;
+            String authStr = "BASIC " + new String(encodeBase64(encodeStr.getBytes()));
+            connection.setRequestProperty("Authorization", authStr);
+          }
+
+          // Set explicitly specified request headers
+          Param header;
+          for (int i = 0; i < headers.size(); i++)
+          {
+            header = (Param)headers.get(i);
+            connection.setRequestProperty(header.getName(), header.getValue());
+          }
+
+          if (method == HTTP_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();
+          }
 
-            //set the timestamp to the file date.
-            long timestamp=0;
+          // Check the response
 
-            boolean hasTimestamp=false;
-            if(useTimestamp && dest.exists()) {
-                timestamp=dest.lastModified();
-                if (verbose)  {
-                    Date t=new Date(timestamp);
-                    log("local file date : "+t.toString());
-                }
-                
-                hasTimestamp=true;
-            }
-        
-            //set up the URL connection
-            URLConnection connection=source.openConnection();
-            //modify the headers
-            //NB: things like user authentication could go in here too.
-            if(useTimestamp && hasTimestamp) {
-                connection.setIfModifiedSince(timestamp);
+          //    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; 
             }
+          }
 
-            //connect to the remote site (may take some time)
-            connection.connect();
-            //next test for 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 = 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 " + source + " to " + dest);
-                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) {
-                fos.write(buffer, 0, length);
-                if (verbose) System.out.print(".");
+          //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;
             }
-            if(verbose) System.out.println();
+            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)  {
-                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 " + source + " to " + dest );
-            if(ignoreErrors) 
-                return;
-            throw new BuildException(ioe, location);
+          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 == HTTP_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.
      *
@@ -228,7 +430,7 @@
      *
      * @param u URL for the file.
      */
-    public void setSrc(URL u) {
+    public void setSrc(String u) {
         this.source = u;
     }
 
@@ -280,4 +482,172 @@
         }
     }
 
+    /**
+     * Set the HTTP request type, <code>post</code> or <code>get</code>
+     */
+    public void setMethod(HttpMethodType method)
+    {
+      if ("post".equalsIgnoreCase(method.getValue()))
+      {
+        this.method = HTTP_POST;
+      }
+      else 
+      {
+        this.method = HTTP_GET;
+      }
+    }
+  
+    public void setAuthtype(AuthMethodType type)
+    {
+      if ("basic".equalsIgnoreCase(type.getValue()))
+      {
+        this.authType = AUTH_BASIC;
+      }
+      else
+      {
+        this.authType = AUTH_NONE;
+      }
+    }
+  
+    public void setUsername(String username)
+    {
+      this.username = username;
+      if (authType == AUTH_NONE)
+      {
+        authType = AUTH_BASIC;
+      }
+    }
+  
+    public void setPassword(String password)
+    {
+      this.password = password;
+    }
+  
+    /**
+     * 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);
+    }
+
+    // This code handles Base64 encoding for basic authentication. I put it here rather than
+    // in a separate class so as to make these changes as self-contained as possible.
+
+    static private char[] alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toCharArray();
+
+    /**
+     * Encode a block of binary data as base64 as specified in RFC1521.
+     * 
+     * @param data   the binary data to encode.
+     * @return An array of characters that represent the data encoded as Base64
+     */
+    static public char[] encodeBase64(byte[] data)
+    {
+      char[] out = new char[((data.length + 2) / 3) * 4];
+
+      //
+      // 3 bytes encode to 4 chars.  Output is always an even
+      // multiple of 4 characters.
+      //
+      for (int i = 0, index = 0; i < data.length; i += 3, index += 4)
+      {
+        boolean quad = false;
+        boolean trip = false;
+
+        int val = (0xFF & (int) data[i]);
+
+        val <<= 8;
+
+        if ((i + 1) < data.length)
+        {
+          val |= (0xFF & (int) data[i + 1]);
+          trip = true;
+        }
+
+        val <<= 8;
+
+        if ((i + 2) < data.length)
+        {
+          val |= (0xFF & (int) data[i + 2]);
+          quad = true;
+        }
+
+        out[index + 3] = alphabet[(quad ? (val & 0x3F) : 64)];
+        val >>= 6;
+        out[index + 2] = alphabet[(trip ? (val & 0x3F) : 64)];
+        val >>= 6;
+        out[index + 1] = alphabet[val & 0x3F];
+        val >>= 6;
+        out[index + 0] = alphabet[val & 0x3F];
+      }
+
+      return out;
+    }
+
+    /** 
+     * This class is used to store name-value pairs for request parameters and headers
+     */
+    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 for "method" with the values "get", and "post".
+     */
+    public static class HttpMethodType extends EnumeratedAttribute {
+        public String[] getValues() {
+            return new String[] {"get", "post"};
+        }
+    }
+
+    /**
+     * Enumerated attribute for "authType" with the value "basic" 
+     * (note, eventually we can add "digest" authentication)
+     */
+    public static class AuthMethodType extends EnumeratedAttribute {
+        public String[] getValues() {
+            return new String[] {"none", "basic"};
+        }
+    }
 }
