/*
 * Software Name : ATK
 *
 * Copyright (C) 2007 - 2012 France Télécom
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * ------------------------------------------------------------------
 * File Name   : Downloader.java
 *
 * Created     : 26/05/2009
 * Author(s)   : Yvain Leyral
 */
package com.orange.atk.atkUI.corecli.utils;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.PasswordAuthentication;
import java.net.Socket;
import java.net.URL;

import org.apache.log4j.Logger;

import com.orange.atk.atkUI.corecli.Alert;

/**
 * A class to handle an HTTP download session, in an independant thread. The
 * class implements the TimerMonitor interface, to allow for abortion of the
 * session upon timeout. The Timers used just have to call the timeout() method
 * to make the session end. There's a timer to guard the connection phase
 * itself, then a second one to guard the data transfer.
 */
public class Downloader extends Thread implements TimerMonitor {

	private DownloadMonitor monitor;
	private String url;
	private File target;
	private int httpMaxConTime, httpMaxDwnTime;
	private Timer connectionTimer;
	private Timer downloadTimer;
	/**
	 * A login to access to the specified URL.
	 */
	private String login;
	/**
	 * A password to access to the specified URL.
	 */
	private String password;

	/**
	 * if the connection requires an authentification of the device.
	 */
	private String userAgent;

	/** stop condition for this thread */
	private boolean shouldStop = false;

	/**
	 * Error message
	 */
	private String errorMessage;

	/**
	 * Construct a Downloader to download the file located at the given URL, and
	 * save it to the target file passed. The max times allowed for timers are
	 * other parameters. The last parameter is the DownloadMonitor to whom
	 * report the evolution of the session).
	 * 
	 * @param url
	 *            The URL to open
	 * @param target
	 *            The target file where to save the downloaded file
	 * @param httpMaxConTime
	 *            The maximum number of seconds allowed before the connection
	 *            timeout
	 * @param httpMaxDwnTime
	 *            The maximum number of seconds allowed before the transfer
	 *            timeout
	 * @param monitor
	 *            The download monitor to use to report changes of the session
	 *            status
	 */
	Downloader(String url, File target, int httpMaxConTime, int httpMaxDwnTime,
			DownloadMonitor monitor) {
		this.url = url;
		this.target = target;
		this.httpMaxConTime = httpMaxConTime;
		this.httpMaxDwnTime = httpMaxDwnTime;
		this.monitor = monitor;
		connectionTimer = new Timer(httpMaxConTime, this);
		downloadTimer = new Timer(httpMaxDwnTime, this);
	}

	/**
	 * Terminates the download session. Dumps the passed message (will be
	 * "(stopped)" if null is passed). Also, changes the status of the
	 * DownloadMonitor provided at creation, as per the one passed. Finally, the
	 * thread is stopped.
	 * 
	 * @param message
	 *            The message to dump
	 * @param status
	 *            The status to report to the associated DownloadMonitor. Valid
	 *            values are the ones defined in the DownloadMonitor class.
	 * @throws Alert
	 */
	public void terminate(String message, int status) {
		if (message == null) {
			message = "(stopped)";
		}
		errorMessage = "Download: " + message + "(" + url + ")";
		monitor.setDownloadStatus(status);
		stopAsap();
	}

	/**
	 * Method of the TimerMonitor interface. A Timer that expires calls that
	 * method, passing a reference to itself. Depending on its identity, the
	 * corresponding message and status are passed to terminate().
	 * 
	 * @param t
	 *            The Timer object that calls that method, so the Downloader can
	 *            identify which timer expired
	 */
	public void timeout(Timer t) {
		if (t == connectionTimer) {
			terminate("Connection attempt aborted, timeout (" + httpMaxConTime + " s).",
					DownloadMonitor.DLOAD_TIMEOUT_C);
		} else
			if (t == downloadTimer) {
				terminate("File transfer aborted, timeout (" + httpMaxDwnTime + " s).",
						DownloadMonitor.DLOAD_TIMEOUT_T);
			} else {
				String msg = "An unknown timer has expired !";
				Logger.getLogger(this.getClass()).error(msg);
				stopAsap();
			}
	}

	/** Stop this thread as soon as possible. */
	public synchronized void stopAsap() {
		shouldStop = true;
		// also stop asap timers
		connectionTimer.stopAsap();
		downloadTimer.stopAsap();
	}

	/**
	 * Main entry point of the thread, when started.
	 */
	public void run() {
		if (isAuthenticate()) {
			Authenticator.setDefault(new MyAuthenticator());
		}
		PrintStream outstream = null;
		HttpURLConnection huc = null;
		OutputStream out = null;
		Socket socket = null;
		InputStream in = null;
		try {
			if (target == null) {
				Logger.getLogger(this.getClass()).warn("target null");
			}
			outstream = new PrintStream(new FileOutputStream(target));
			URL urlObj = new URL(url);

			if ((userAgent != null) && (userAgent.trim().length() > 0)) {
				if (userAgent.indexOf('\n') > 0) {
					Logger.getLogger(this.getClass()).warn(
							"INVALID user_agent: '" + userAgent
									+ "' : contains a newline character --> No user agent used");
				} else {
					System.getProperties().put("http.agent", userAgent);
				}
			}

			huc = (HttpURLConnection) urlObj.openConnection();
			connectionTimer.start();
			huc.connect();
			if (!shouldStop) { // no timeout occurs during connection
				connectionTimer.stopAsap();
				int code = huc.getResponseCode();
				if (code != 200) {
					huc.disconnect();
					if (code == 404 || code == 401 || code == 502) {
						terminate("Connection failed. Server returned HTTP code " + code + ".",
								DownloadMonitor.DLOAD_FAILED_NO_NEW_ATTEMPT);
					} else {
						terminate("Connection failed. Server returned HTTP code " + code + ".",
								DownloadMonitor.DLOAD_FAILED);
					}
				} else {
					// Get an input stream for reading
					in = huc.getInputStream();
				}
			}
			if (in != null) {
				BufferedInputStream bufIn = new BufferedInputStream(in);

				// Read bytes until end of stream, and write them to local file
				downloadTimer.start();
				int data = bufIn.read();
				while ((!shouldStop) && (data != -1)) {
					outstream.write(data);
					data = bufIn.read();
				}
				bufIn.close();
				if (!shouldStop) { // no timeout occurs during download
					downloadTimer.stopAsap();
					terminate("Completed.", DownloadMonitor.DLOAD_SUCCESS);
				}
			}
			if (huc != null) {
				huc.disconnect();
			}
		} catch (MalformedURLException e) {
			terminate("Malformed URL", DownloadMonitor.DLOAD_FAILED);
		} catch (FileNotFoundException e) {
			terminate("Can't create file:" + target.getAbsolutePath(), DownloadMonitor.DLOAD_FAILED);
		} catch (IOException e) {
			terminate("I/O Error - " + e, DownloadMonitor.DLOAD_FAILED);
		} catch (SecurityException e) {
			terminate("Can't create file (security issue):" + target.getAbsolutePath(),
					DownloadMonitor.DLOAD_FAILED);
		} finally { // do it what ever exception occurs or not

			System.getProperties().remove("http.agent");

			try {
				if (out != null)
					out.close();
				if (socket != null)
					socket.close();
				if (in != null)
					in.close();
				if (outstream != null)
					outstream.close();
			} catch (IOException e) {
				terminate("I/O Error - " + e, DownloadMonitor.DLOAD_FAILED);
			}
		}
	}

	/**
	 * This class allows making a connection with an authentication (login and
	 * password).
	 * 
	 * @author apenault
	 * 
	 */
	public class MyAuthenticator extends Authenticator {
		// This method is called when a password-protected URL is accessed
		protected PasswordAuthentication getPasswordAuthentication() {
			return new PasswordAuthentication(login, password.toCharArray());
		}
	}

	/**
	 * To know if the connection must be authenticated by a login and a password
	 * 
	 * @return true if the connection to the given URL requires a login and a
	 *         password
	 */
	public boolean isAuthenticate() {
		return (login != null && password != null);
	}

	/**
	 * Sets the value of User-Agent to use for the HTTP connection.
	 * 
	 * @param userAgent
	 */
	public void setUserAgent(String userAgent) {
		this.userAgent = userAgent;
	}

	public void setLogin(String login) {
		this.login = login;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getErrorMessage() {
		return errorMessage;
	}
}