
import java.net.*;
import java.io.*;
import java.util.*;
import java.applet.*;


     //--
     //-- append(StringBuffer) is java 1.4
     //-- URL.getPath is java 1.3
     //-- working on java 1.1 virtual machine
//--------------------------------------------
/**
 *  This class represents a download of a web file.
 *  This class will store data about the process of attempting
 *  to download the resource from the internet. The class
 *  will also be responsable for actually initiating the download
 *  process by opening a connection to the resource etc.
 *  
 *  This was adapted from the SoundDownload class
 *  @see "SoundDownload"
 *  @author matth3wbishop<at>yahoo<dot>com
 */ 

public class WebDownload extends Object implements Task
{
  //--------------------------------------------
  /** the resource to get */
  private String url;
  //--------------------------------------------
  /** The time at which the attempted download was begun */
  private Date startTime;
  //--------------------------------------------
  /** The time at which the attempted download was terminated
   *  whether successfully or unsuccessfully */
  private Date finishTime;
  //--------------------------------------------
  /** A place to store the downloaded resource,  */
  private String fileName;
  //--------------------------------------------
  /** A directory in which to put files  */
  private String directoryName = "";
  //--------------------------------------------
  /** whether to overwrite existing files */
  private boolean overwriteFiles = false;
  //--------------------------------------------
  /** whether to reget files that already exist */
  private boolean regetExisting;
  //--------------------------------------------
  /** will store error messages produced when attempting to download
   *  the file, such as those produced by the Url class in
   *  attempting to make a connection to the resource. */ 
  private StringBuffer errorMessage;

  //--------------------------------------------
  /** The protocol of the url  */
  private String protocol;
  //--------------------------------------------
  private int httpResponseCode;
  //--------------------------------------------
  private String httpResponseMessage;
  //--------------------------------------------
  private String httpContentType;
  //--------------------------------------------
  private int httpContentLength;
  //--------------------------------------------
  private String httpContentEncoding;

  //--------------------------------------------
  /** The number of bytes already read from the resource */
  private long bytesRead;
  //--------------------------------------------
  /** a chunk for reading from the target, in bytes */
  private static int CHUNK_SIZE = 10000;
  //--------------------------------------------
  /** a default file name for downloaded resource */
  private String DEFAULT_FILE_NAME = "index";
  //--------------------------------------------
  /** the progress marker */
  private String PROGRESS_MARKER = ".";


  //--------------------------------------------
  /** the reason the download failed  */
  private String failureCause;
  //--------------------------------------------
  private boolean wasSuccessful;
  //--------------------------------------------
  /** whether the download has started */
  private boolean hasStarted;
  //--------------------------------------------
  private boolean hasFinished;
  //--------------------------------------------
  private boolean showProgress = true;
  //--------------------------------------------
  public static String NEWLINE = System.getProperty("line.separator");

  //--------------------------------------------
  //-- CONSTRUCTORS

  public WebDownload()
  {
    this.initialize();
  } //-- constr: ()

  //--------------------------------------------
  public WebDownload(String sUrl)
  {
    this.initialize();
    this.setUrl(sUrl);
  } 


  //-- METHODS

  //--------------------------------------------
  /*  */
  public void initialize()
  {

    this.overwriteFiles = false;
    this.regetExisting = true;
    this.showProgress = true;
    this.directoryName = "";

    this.reset();
  }

  //--------------------------------------------
  /** this method sets most of the internal fields to
   *  nothing. this method is mainly to allow one object
   *  to be reused for several downloads.  */
  public void reset()
  {
    this.protocol = "";
    this.httpContentType = "";
    this.httpContentLength = 0;
    this.httpContentEncoding = "";
    this.httpResponseCode = 0;
    this.httpResponseMessage = "";
    this.bytesRead = 0;

    this.fileName = "";
    this.failureCause = "";
    this.errorMessage = new StringBuffer("");    
    this.hasStarted = false;
    this.wasSuccessful = false;
    this.hasFinished = false;
    this.url = new String("");
    this.startTime = new Date(0);
    this.finishTime = new Date(0);

    //-- show progress should not be reset
    //-- the directory name should not be reset
    //-- regetExisting should not be reset
    //-- overwriteFiles should not be reset;

  } //-- method: reset

  //--------------------------------------------
  /* return the target to download */
  public String getUrl()
  {
    return this.url;
  }

  //--------------------------------------------
  public void setUrl(String sUrl)
  {
    this.reset();
    if (sUrl.toLowerCase().startsWith("www."))
     { this.url = "http://" + sUrl; }
    else
     { this.url = sUrl; }
  }

  //--------------------------------------------
  public void setLocalFileName(String sLocalFileName)
  {
    this.fileName = sLocalFileName;
  }

  //--------------------------------------------
  /** tries to set the local directory. If a bad string is
   *  given it returns false  */

  public boolean setLocalDirectory(String sLocalDirectoryName)
  {
    File fDirectory = new File(sLocalDirectoryName);

    if (!fDirectory.exists())
     { return false; }

    if (!fDirectory.isDirectory())
    {
      return false;
    }

    //-- remove a final path separator from the name
    if (sLocalDirectoryName.endsWith(File.separator))
    {
      this.directoryName =
       sLocalDirectoryName.substring(0, sLocalDirectoryName.length() - 1);
    }
    else
    {
      this.directoryName = sLocalDirectoryName;
    }

    return true;
  } //--

  //--------------------------------------------
  /* advises whether the attempted download was carried out successfully */
  public boolean wasSuccessful() 
    { return this.wasSuccessful; }


  //--------------------------------------------
  /** sets whether existing files are redownloaded */
  public void regetExisting() 
    { this.regetExisting = true; }

  //--------------------------------------------
  /** sets whether existing files are redownloaded */
  public void regetExisting(boolean bReget) 
    { this.regetExisting = bReget; }

  //--------------------------------------------
  /** sets whether existing files are overwritten */
  public void overwrite() 
    { this.overwriteFiles = true; }

  //--------------------------------------------
  /** sets whether existing files are overwritten */
  public void overwrite(boolean bOverwrite) 
    { this.overwriteFiles = bOverwrite; }


  //--------------------------------------------
  /** whether some indicator is printed to the console */
  public void showProgress() 
    { this.showProgress = true; }

  //--------------------------------------------
  public void showProgress(boolean bShow) 
    { this.showProgress = bShow; }

  //--------------------------------------------
  public void setProgressMarker(String sMarker) 
    { this.PROGRESS_MARKER = sMarker; }

  //--------------------------------------------
  /* informs of the average download speed */
  public float getAverageSpeed() 
  {
    float fAverageSpeed = 0;
    if (!this.wasSuccessful)
      { return 0; }
 
    //-- 
    fAverageSpeed = (float)this.bytesRead/this.getDownloadDuration();
    return fAverageSpeed;
  }

  //--------------------------------------------
  /** an alias to comply with the task interface */
  public void doTask()
  {
    this.download();
  }

  //--------------------------------------------
  /** attempt the download and fail with dignity and grace */
  public void download()
  {
    //-- 
    URL uWebResource;
    URLConnection urlc;
    HttpURLConnection httpc;

    this.hasStarted = true;
    this.startTime = new Date();

    try
    {
      uWebResource = new URL(this.url);      
      this.protocol = uWebResource.getProtocol();
    }
    catch (MalformedURLException e)
    {
      this.failureCause = "Illegal Url";
      this.errorMessage.append(e);
      this.wasSuccessful = false;
      this.finishTime = new Date();
      this.hasFinished = true; 
      return;
    }  //-- try
      
    try
    {
      urlc = uWebResource.openConnection();
    }     
    catch (IOException e)
    {
      this.failureCause = "Couldnt create a connection";
      //-- 
      //this.errorMessage.append(e.getMessage());
      this.errorMessage.append(e);
      this.wasSuccessful = false;
      this.finishTime = new Date();
      this.hasFinished = true; 
      return;
    }  //-- try

    try
    {
      urlc.connect();
    }     
    catch (UnknownHostException e)
    {
      this.failureCause =
        "The host '" + uWebResource.getHost() + "' could not be found";
      this.errorMessage.append(e);
      this.wasSuccessful = false;
      this.finishTime = new Date();
      this.hasFinished = true; 
      return;
    } 
    catch (IOException e)
    {
      this.failureCause = "Couldnt establish a connection to URL";
      this.errorMessage.append(e);
      this.wasSuccessful = false;
      this.finishTime = new Date();
      this.hasFinished = true; 
      return;
    } //-- try


    /* cast the connection object so as to find out the precise cause
       of any download problems */
    if (uWebResource.getProtocol().equalsIgnoreCase("http"))
    {
      httpc = (HttpURLConnection)urlc;
      try
      {
        this.httpResponseCode = httpc.getResponseCode();
        this.httpResponseMessage = httpc.getResponseMessage();
      }
      catch (IOException e)
      {     
      }

      if (this.httpResponseCode == 404)
      {
        this.finishTime = new Date();
        this.wasSuccessful = false;
        this.hasFinished = true;
        this.failureCause = "file not found on server";
        this.errorMessage.append(
          "Message from host '" + uWebResource.getHost() + "'" + NEWLINE +
          this.httpResponseCode + " " + this.httpResponseMessage);
        return;
      }


      if ((this.httpResponseCode < 200) || (this.httpResponseCode > 299))
      {
        this.finishTime = new Date();
        this.wasSuccessful = false;
        this.hasFinished = true;
        this.failureCause = "server refused request";
        this.errorMessage.append(NEWLINE +
          "Server refused request : " + NEWLINE +
          "Server response code   : " + this.httpResponseCode + NEWLINE +
          "Server response message: " + this.httpResponseMessage + NEWLINE);
        return;
      }
     
      this.httpContentType = httpc.getContentType();
      this.httpContentLength = httpc.getContentLength();
      this.httpContentEncoding = httpc.getContentEncoding();


      //-- the static method below may be useful to do further checks 
      //-- on the content type of the resource.
      //URLConnection.guessContentTypeFromStream(inputstream);
    } //-- if is http


    File fSaveFile;
    String sFileName;

    String sFileNameSuffix;
    if (this.httpContentType.startsWith("text"))
      { sFileNameSuffix = ".txt"; }
    else if (this.httpContentType.startsWith("application"))
      { sFileNameSuffix = ".app"; }
    else
      { sFileNameSuffix = ""; }


    //--  get path is 1.4   
    //--  StringBuffer sbUrlPath = new StringBuffer(uWebResource.getPath());
    StringBuffer sbUrlPath = new StringBuffer(uWebResource.getFile());
    //--
    //-- if the url has no file name, add something
    if (sbUrlPath.toString().trim().endsWith("/"))
    {
      if (this.httpContentType.trim().equalsIgnoreCase("text/html"))
      {
        sbUrlPath.append(this.DEFAULT_FILE_NAME + ".html");
      }
      else if (this.httpContentType.startsWith("text"))
      {
        sbUrlPath.append(this.DEFAULT_FILE_NAME + ".txt");
      }
      else
      {
        sbUrlPath.append(this.DEFAULT_FILE_NAME + sFileNameSuffix);
      }
    } //-- if

    if (sbUrlPath.toString().trim().equalsIgnoreCase(""))
      { sbUrlPath.append(this.DEFAULT_FILE_NAME + sFileNameSuffix); }


     //-- get rid of the url path to just leave the file name
     //-- Under MS jview getName returns the full path not just
     //-- the last component
     //-- sFileName = (new File(sbUrlPath.toString())).getName();
     //--
     //-- Replace some potentially bad characters as well
     sFileName =
       sbUrlPath.toString().substring(
         sbUrlPath.toString().lastIndexOf('/') + 1)
          .replace('\\', '-')
          .replace('%', '-')
          .replace('=', '-')
          .replace('?', '-')
          .replace('*', '-');


     //-- if no filename is given, use the Url filename, otherwise
     //-- use the given name.
      StringBuffer sbFileName;

      if (this.fileName == "")
      {
        sbFileName = new StringBuffer(sFileName);
      }
      else
      {
        sbFileName = new StringBuffer(this.fileName);
      }

      //-- prefix the directory name
      if (this.directoryName != "")
      {
        sbFileName.insert(0, this.directoryName + File.separator);
      }

      String sOriginalFileName = new String(sbFileName);


      boolean bFileExists = (new File(sbFileName.toString())).exists();

      if (bFileExists && !this.regetExisting)
      {
        this.failureCause = "File exists and reget flag is false";
        this.errorMessage.append(
         "A file of the given name already exists and the ");
        this.errorMessage.append(
         "'regetExisting' flag of the object is set to false. ");
        this.errorMessage.append(
         "In other words the object is not supposed to download ");
        this.errorMessage.append(
         "files where the local file already exists. ");
        this.errorMessage.append(
         "You can use the WebDownload.regetExisting() method");
        this.errorMessage.append(
         "to change this behavior. ");
        this.wasSuccessful = false;
        this.finishTime = new Date();
        this.hasFinished = true;
        return;
      }

      //--
      //-- get a file name which doesnt exist already by
      //-- adding a numeric suffix
      //--

      if (!this.overwriteFiles)
      {
        int ii = 0;
        while ((new File(sbFileName.toString())).exists())
        {
          ii++;
          sbFileName.setLength(0);
          sbFileName.append(sOriginalFileName);
          sbFileName.append("." + Integer.toString(ii));
        }
      } //-- if not overwrite


      this.fileName = sbFileName.toString();

      fSaveFile = new File(sbFileName.toString());
      if (fSaveFile.isDirectory())
      {
        this.wasSuccessful = false;
        this.failureCause = "Local file name is a directory";
        this.errorMessage.append(
         "The local file name '" + this.fileName + "' is a directory");
        this.finishTime = new Date();

        return;
      }

      InputStream isResource;
      FileOutputStream fout;

      try
      {
        isResource = uWebResource.openConnection().getInputStream();
      }
      catch (IOException e)
      {
        this.failureCause = "Couldnt open the inputstream";
        this.errorMessage.append(e);
        this.wasSuccessful = false;
        this.finishTime = new Date();
        this.hasFinished = true;
        return;
      }

      try
      {
        fout = new FileOutputStream(fSaveFile);
      }
      catch (FileNotFoundException e)
      {
        
        this.failureCause = "Local save path is invalid";

        this.errorMessage.append(e.toString());
        this.errorMessage.append(
          " The path given for saving the downloaded file ");
        this.errorMessage.append("locally (");
        this.errorMessage.append(fSaveFile.toString());
        this.errorMessage.append(") is not valid. ");
        this.errorMessage.append(
          "This may mean that one or more of the directories in the ");
        this.errorMessage.append(
          "path does not exist. or that the file name has illegal");
        this.errorMessage.append(
          "characters in it.");

        this.wasSuccessful = false;
        this.finishTime = new Date();
        this.hasFinished = true;
        
        return;
      } //-- try
      catch (IOException e)
      {
        this.wasSuccessful = false;
        this.finishTime = new Date();
        this.hasFinished = true;
        
        return;
      } //-- try


      int iByte;
      int iCounter = 0;

      try
      {
        iByte = isResource.read();

        while (iByte != -1)
        {
          while ((iByte != -1) && (iCounter < this.CHUNK_SIZE))
          {
        
            fout.write(iByte);
            iCounter++;
            iByte = isResource.read();
          } //-- while

          this.bytesRead += iCounter;
          iCounter = 0;
          if (this.showProgress)
           { System.out.print(this.PROGRESS_MARKER); }

        } //-- while 
      }
      catch (IOException e)
      {
        this.failureCause =
         "problem reading from the stream";
        this.errorMessage.append(e);
        this.wasSuccessful = false;
        this.finishTime = new Date();
        this.hasFinished = true;
        return;
      } //-- try


      try
      {
        isResource.close();
        fout.close();
      }
      catch (IOException e)
      {
        this.failureCause =
         "problem closing the streams";
        this.errorMessage.append(e);
        this.wasSuccessful = false;
        this.finishTime = new Date();
        this.hasFinished = true;
        return;
      } //-- try


      this.finishTime = new Date();
      this.wasSuccessful = true;
      this.hasFinished = true;
  } //-- method: download()


  //--------------------------------------------
  /** an alias to comply with the task interface */
  public String printReport()
  {
    return this.printStatistics();
  }

  //--------------------------------------------
  /** prints information such as the start time, the finish
   *  time, the duration and the success or failure of the 
   *  download. */
  public String printStatistics()
  { 
    StringBuffer sbOutput = new StringBuffer("");
    sbOutput.append(NEWLINE);
    sbOutput.append("Attempted to get resource >");
    sbOutput.append(this.url);
    sbOutput.append(NEWLINE);
    sbOutput.append("And saving to file name   >");
    sbOutput.append(this.fileName);
    sbOutput.append(NEWLINE);
    sbOutput.append("in the directory          >");
    sbOutput.append(this.directoryName);
    sbOutput.append(NEWLINE);

    if (this.wasSuccessful)
    {
      sbOutput.append("Result of the download    >");
      sbOutput.append("SUCCESS");
      sbOutput.append(NEWLINE);
    }
    else
    {
      sbOutput.append("Result of the download    >");
      sbOutput.append("FAILURE");
      sbOutput.append(NEWLINE);
    }

    sbOutput.append("Download duration (s)            >");
    sbOutput.append((float)this.getDownloadDuration()/1000);
    sbOutput.append(NEWLINE);
    sbOutput.append("Total bytes read                 >");
    sbOutput.append(this.bytesRead);
    sbOutput.append(NEWLINE);
    sbOutput.append("Average download speed (kbyte/s) >");
    sbOutput.append(this.getAverageSpeed());
    sbOutput.append(NEWLINE);


    if (!this.failureCause.equals(""))
    {
      sbOutput.append("Cause of download failure        >");
      sbOutput.append(this.failureCause);
      sbOutput.append(NEWLINE);
    }

    sbOutput.append("Protocol used         >");
    sbOutput.append(this.protocol);
    sbOutput.append(NEWLINE); 
    sbOutput.append("Http Content Type     >");
    sbOutput.append(this.httpContentType);
    sbOutput.append(NEWLINE);
    sbOutput.append("Http Content Length   >");
    sbOutput.append(this.httpContentLength);
    sbOutput.append(NEWLINE);
    sbOutput.append("Http Content Encoding >");
    sbOutput.append(this.httpContentEncoding);
    sbOutput.append(NEWLINE);
    sbOutput.append("Start Time   >");
    sbOutput.append(this.startTime);
    sbOutput.append(NEWLINE);
    sbOutput.append("Finish Time  >");
    sbOutput.append(this.finishTime);
    sbOutput.append(NEWLINE);

    if (!this.errorMessage.toString().equals(""))
    {
      sbOutput.append("Error Messages generated during the download >");
      sbOutput.append(NEWLINE);
      sbOutput.append(this.errorMessage.toString());
      sbOutput.append(NEWLINE);
    }

    sbOutput.append("");
    sbOutput.append("");
    sbOutput.append("");
    return sbOutput.toString();    
  }
  
  //--------------------------------------------
  public String getFileName()
  {
    return this.fileName;
  }


  //--------------------------------------------
  /* return the number of milliseconds taken to download the file. */
  public long getDownloadDuration()
  {
    if (this.finishTime.getTime() == 0) { return 0; }
    if (this.startTime.getTime() == 0) { return 0; }
    return this.finishTime.getTime() - this.startTime.getTime();
  } 

  //--------------------------------------------
  /** print a message to reassure the user */ 
  public String printStartMessage() 
  {
    StringBuffer sbReturn = new StringBuffer("");
    sbReturn.append("Attempting to get " + this.url);
    if (this.showProgress)
    {
      sbReturn.append(", each [" + this.PROGRESS_MARKER + "]");
      sbReturn.append("=" + this.CHUNK_SIZE + " bytes");
    }
    else
    {
      sbReturn.append(" (no progress marker)");
    }
    sbReturn.append(NEWLINE);
    return sbReturn.toString(); 

  } //-- method: printStartMessage


  //--------------------------------------------
  /* how is this different from toString */
  public String print()
  { 
    return this.printStatistics();
  }

  //--------------------------------------------
  /** a concise summary of what happened */
  public String toString()
  {

    StringBuffer sbOutput = new StringBuffer("");
    sbOutput.append("resource download ");


    sbOutput.append("(url=");
    sbOutput.append(this.url);
    sbOutput.append(")");
    sbOutput.append(" --> \"");

    if (!this.directoryName.equals(""))
    {
      sbOutput.append(this.directoryName);
      sbOutput.append(File.separator);
    }
    sbOutput.append(this.fileName);
    sbOutput.append("\"");

    if (this.wasSuccessful)
    {
      sbOutput.append(" -successful-");
    }
    else
    {
      sbOutput.append(" -failed-");
    }

    sbOutput.append(" took:");
    sbOutput.append((float)this.getDownloadDuration()/1000);
    sbOutput.append("s");
    sbOutput.append(", size:");
    sbOutput.append(this.bytesRead);
    //sbOutput.append(", speed (kb/s):");
    //sbOutput.append(this.getAverageSpeed());


    if (!this.httpContentType.equals(""))
    {
      sbOutput.append(", content:");
      sbOutput.append(this.httpContentType);
    }

    sbOutput.append(" -at- ");
    sbOutput.append(this.startTime);
    sbOutput.append(NEWLINE);

    sbOutput.append("");
    return sbOutput.toString();    

  } //-- method: toString()

  //--------------------------------------------
  /** to allow for testing and one-off downloads */
  public static void main(String[] args) throws Exception
  {
    String sUsageMessage = "usage: java WebDownload url [dir]";
    String sDirectory = new String("");

    if (args.length == 0)
    {	    
      System.out.println(sUsageMessage);
      System.exit(-1);
    }

    if (args.length == 2)
    {
      sDirectory = args[1];
    }

    StringBuffer sOutput = new StringBuffer("");
    String sTest = new String(args[0]);

    WebDownload wdTest = new WebDownload(sTest);
    wdTest.showProgress();

    if (!wdTest.setLocalDirectory(sDirectory))
    {
      System.out.println(
       "The directory you specified was not found,");
      System.out.println(
       "using the current directory instead.");
    }
   
    System.out.println(wdTest.printStartMessage());
    wdTest.download();
    System.out.println(wdTest.printStatistics());
    System.out.println(wdTest.toString());

  } //-- main()
} //-- WebDownload class
