/*$Id: DownloadManager.java,v 1.3 2011/09/21 13:04:16 manikandanmv Exp $*/
package com.adventnet.sym.server.downloadmgr;

import java.util.logging.Level;
import java.util.logging.Logger;

import java.util.Properties;
import java.io.InputStream;
import java.io.FileOutputStream;

import java.net.URL;

import HTTPClient.HTTPConnection;
import HTTPClient.HTTPResponse;
import HTTPClient.ModuleException;
import HTTPClient.NVPair;
import HTTPClient.ProtocolNotSuppException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Enumeration;


/**
 * This class can be used for downloading the files from the external sites using
 * http and https format.
 * @author rajeshr
 */
public class DownloadManager {

    private Logger logger = Logger.getLogger("DownloadManager");
    public static DownloadManager dwnmgr = null;
    private Properties proxydetails = null;

    /**
     * This creates a new instance for DownloadManager when it is not created for this jvm.
     * @return instance of DownloadManager
     */
    public static DownloadManager getInstance() {
        if (dwnmgr == null) {
            dwnmgr = new DownloadManager();
        }
        return dwnmgr;
    }

    /**
     * Set the proxy details to be used by download manager for downloading the files through
     * proxy server if any. Whenever any changes are occurred in the proxy details, using this
     * method we need to update the download manager.
     * @param proxyProp - holds the informations of proxy server, proxy port, username and
     * password.
     */
    public void setProxyConfiguration(Properties proxyProp) {
        this.proxydetails = proxyProp;
    }

    /**
     * This method will be used for validating whether the provided details are correct and
     * able to download the binary file using this credential details from sync.patchquest.com
     * @param proxyProp - holds the informations of proxy server, proxy port, username and password.
     * @return - when the binary is able to be downloaded then the flag returned is true otherwise
     * false.
     * @throws IOException
     * @throws ModuleException
     */
    public boolean TestConnection(Properties proxyProp) throws IOException, ModuleException {
        boolean returnStatus = false;
        HTTPResponse response = null;
        String sourceFile = "https://sync.patchquest.com/crs/testsample.exe"; // NO I18N
        response = getHttpResponse(sourceFile, null, proxydetails,null);
        if (response != null) {
            int status = response.getStatusCode();
            if (status >= 200 && status < 400) {
                String rspContentType = response.getHeader("Content-Type");
                if (rspContentType != null && rspContentType.indexOf("text/") == -1) {
                    returnStatus = true;
                }
            }
        }
        return returnStatus;
    }

    /**
     * This method is called from other modules to download the file from the other sites and store
     * the data in the specified destination file.
     * @param sourceFile - This is source file where the data need to be downloaded. The file path need to be specified in http format
     * @param destinationFile - This is destination file where the retrieved data need to be stored.
     * @return DownloadStatus
     */
    public DownloadStatus downloadFile(String sourceFile, String destinationFile) {
        return downloadFile(sourceFile, destinationFile, null, null, false);
    }

    /**
     * This method is called from other modules to download the file from the other sites and store
     * the data in the specified destination file.
     * @param sourceFile - This is source file where the data need to be downloaded. The file path need to be specified in http format
     * @param destinationFile - This is destination file where the retrieved data need to be stored.
     * @param formdata - This holds the key and value which needs to be sent in the http request.
     * @return DownloadStatus
     */
    public DownloadStatus downloadFile(String sourceFile, String destinationFile, Properties formdata) {
        return downloadFile(sourceFile, destinationFile, formdata, null, false);
    }

    /**
     * This method can be used for getting the data from the web url and process the data on the fly instead of storing the data in file system.
     * @param sourceFile - This is the source file where the data need to be retrieved.
     * @param postformdata - This holds the key and value which needs to be sent in the http request.
     * @return responseBuffer - The content of the source file returned in buffer. The response buffer
     * will be null when the http response is ended up in error.
     */
    public DownloadStatus GetFileDataBuffer(String sourceFile, Properties postformdata) {
        return downloadData(sourceFile, postformdata);
    }

    /**
     * This method is called from other modules to download the file from the other sites and store
     * the data in the specified destination file.
     * @param sourceFile - This is source file where the data need to be downloaded. The file path need to be specified in http format
     * @param destinationFile - This is destination file where the retrieved data need to be stored.
     * @param postformdata - This holds the key and value which needs to be sent in the http request.
     * @param isBinary - To ensure whether the proper content type has been downloaded from the web server.
     * For eg. when the value is passed as false, then the download will be considered as success only
     * when the response content type should not contains the "text/".
     */
    private DownloadStatus downloadFile(String sourceFile, String destinationFile, Properties postformdata, Properties headers, boolean isBinary) {
        InputStream in = null;
        boolean continueDownload = true;
        HTTPResponse response = null;

        int returnStatus = DownloadConstants.ERROR_DOWNLOAD_FAILED;
        String errorMessage = "";

        try {
            logger.log(Level.INFO, "Going to establish connecton for "+sourceFile);
            String userAgent = System.getProperty("http.agent");
            if (userAgent == null) {
                userAgent = "ManageEngine Desktop Central";//No I18N
            }
            if (headers == null) {
                headers = new Properties();
            }
            if(!headers.containsKey("User-Agent"))
            {
            headers.put("User-Agent", userAgent);
            }
            response = getHttpResponse(sourceFile, postformdata, proxydetails, headers);
            //response = null;
            if (response != null) {
                int httpstatus = response.getStatusCode();
                if (httpstatus >= 200 && httpstatus < 400) {
                    if (isBinary) {
                        String rspContentType = response.getHeader("Content-Type");
                        if (rspContentType != null && rspContentType.indexOf("text/") != -1) {
                            continueDownload = false;
                        }
                    }

                    if (continueDownload) {
                        in = response.getInputStream();
                        try {
                            if (in != null) {
                                logger.log(Level.INFO, "Reading the stream of the url ...{0}", sourceFile);
                                int loopCounter = 0;
                                while (in.available() < 1 && loopCounter < 6) {
                                    loopCounter++;
                                    // Since the response time from web server might take few seconds. Hence sleep is introduced.
                                    Thread.sleep(10000);
                                }
                            }
                        } catch (InterruptedException iex) {
                            logger.log(Level.INFO, "The Patch Download thread in sleep exception : {0}", iex.getMessage());
                        }

                        // read the input stream and write to the buffer or output stream
                        if (in != null && in.available() > 0) {
                            if (destinationFile != null) {
                                writeFile(in, destinationFile);
                                returnStatus = DownloadConstants.SUCCESS;
                            }
                        } else {
                            logger.log(Level.INFO, "The in stream is empty for the url {0}", sourceFile);
                        }
                    }
                } else {
                    if (httpstatus == 403) {
                        returnStatus = httpstatus;
                        errorMessage = "The request is forbidden.Http Status Code :: 403";  // No I18N
                    } else {
                        logger.log(Level.WARNING, "The http status code returned from web server : {0}", new Integer(httpstatus));
                        errorMessage = "The http status code returned from web server : " + new Integer(httpstatus); // No I18N
                    }
                }
            } else {
                logger.log(Level.WARNING, "The received response is null for the source url : {0}", sourceFile);
                errorMessage = "The received response is null for the source url : " + sourceFile; // No I18N
            }
        } catch (ProtocolNotSuppException pnse) {
            logger.log(Level.WARNING, "Error occurred while reading & writing : {0} : {1}", new Object[]{sourceFile, pnse});
            errorMessage = pnse.getMessage();
        } catch (FileNotFoundException fexp) {
            logger.log(Level.WARNING, "Error occurred while writing to the file : {0} : {1}", new Object[]{destinationFile, fexp});
            errorMessage = fexp.getMessage();
        } catch (IOException ex) {
            logger.log(Level.WARNING, "Error occurred while reading & writing : {0} : {1}", new Object[]{destinationFile, ex});
            errorMessage = ex.getMessage();
        } catch (Exception exp) {
            logger.log(Level.WARNING, "Runtime Exception has been occurred while downloading : {0} : {1}", new Object[]{destinationFile, exp});
            errorMessage = exp.getMessage();
            exp.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception ex) {
                logger.log(Level.WARNING, "Error while closing input streams {0}", ex);
            }
        }
        DownloadStatus status = new DownloadStatus(returnStatus, errorMessage);
        return status;
    }

    /**
     * This method is called from other modules to download the file data from the other sites
     * and return the data in the responseBuffer.
     * @param sourceFile - This is source file where the data need to be downloaded. The file path need to be specified in http format
     * @param destinationFile - This is destination file where the retrieved data need to be stored.
     * @return status - The download status, errorMessage and url data buffer are updated in DownloadStatus and returned.
     */
    private DownloadStatus downloadData(String sourceFile, Properties postformdata) {
        InputStream in = null;
        HTTPResponse response = null;
        String responseBuffer = null;
        String errorMessage = "";
        int returnStatus = DownloadConstants.ERROR_DOWNLOAD_FAILED;

        try {
            response = getHttpResponse(sourceFile, postformdata, proxydetails,null);
            if (response != null) {
                int httpstatus = response.getStatusCode();
                if (httpstatus >= 200 && httpstatus < 400) {
                    in = response.getInputStream();
                    try {
                        if (in != null) {
                            logger.log(Level.INFO, "Reading the stream of the source url ...{0}", sourceFile);
                            int loopCounter = 0;
                            while (in.available() < 1 && loopCounter < 6) {
                                loopCounter++;
                                // Since the response time from web server might take few seconds. Hence sleep is introduced.
                                Thread.sleep(10000);
                            }
                        }
                    } catch (InterruptedException iex) {
                        logger.log(Level.WARNING, "The Patch Download thread in sleep exception : {0}", iex);
                    }

                    // read the input stream and write to the buffer or output stream
                    if (in != null && in.available() > 0) {
                        responseBuffer = readDataBufferFromStream(in);
                        returnStatus = DownloadConstants.SUCCESS;
                    } else {
                        logger.log(Level.INFO, "The in stream is empty for the url {0}", sourceFile);
                    }
                } else {
                    logger.log(Level.WARNING, "The http status code returned from web server : {0}", new Integer(httpstatus));
                    errorMessage = "The http status code returned from web server : " + new Integer(httpstatus); // No I18N
                }
            }
        } catch (IOException ex) {
            logger.log(Level.WARNING, "Error occurred while reading source file : {0} : {1}", new Object[]{sourceFile, ex});
            errorMessage = ex.getMessage();
        } catch (Exception exp) {
            logger.log(Level.WARNING, "Runtime Exception has been occurred while reading source file : {0} : {1}", new Object[]{sourceFile, exp});
            errorMessage = exp.getMessage();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception ex) {
                logger.log(Level.WARNING, "Error occurred while closing input streams {0}", ex);
            }
        }
        DownloadStatus status = new DownloadStatus(returnStatus, errorMessage);
        status.setUrlDataBuffer(responseBuffer);
        return status;
    }

    /**
     * This method is used to get the HTTPResponse by establishing the connection with the source url.
     * @param sourceFile - This is source file where the data need to be downloaded. The file path need to be specified in http format.
     * @param postformdata - This holds the key and value which needs to be sent in the http request.
     * @param proxydetails - proxy server details and credentials are provided.
     * @return response - HTTPClient.HTTPResponse is returned.
     */
    private HTTPResponse getHttpResponse(String sourceFile, Properties postformdata, Properties proxydetails, Properties headers) throws ProtocolNotSuppException, MalformedURLException, IOException, ModuleException {
        HTTPResponse response = null;
        HTTPConnection connection = null;

        if (sourceFile != null) {
            SSLUtil sslutil = SSLUtil.getInstance();
            URL url = new URL(sourceFile);
            connection = sslutil.getConnection(sourceFile, proxydetails);
            if (postformdata != null) {
                NVPair[] nvpairs = getPostFormData(postformdata);
                NVPair[] headersNVPair = null;
                
                if ( headers != null ) {
                headersNVPair = getPostFormData(headers);
            }
                
                if (nvpairs != null) {
                    response = connection.Get(url.getFile(), nvpairs, headersNVPair);
                } else {
                    response = connection.Get(url.getFile());
                }
            } else {
                response = connection.Get(url.getFile());
            }
        }
        return response;
    }

    /**
     * This method is used to write the content by reading the data from input stream.
     * @param in : InputStream available from the download file location.
     * @param destinationFile : The data retrieved from stream is written to the specified destination file.
     * If the directory path is not available for destination file, then it will create the required parent
     * directories.
     */
    private void writeFile(InputStream in, String destinationFile) throws IOException, FileNotFoundException,Exception {
        FileOutputStream outFile = null;
        try {
            if (destinationFile != null) {
                File fname = new File(destinationFile).getParentFile();
                if (!fname.exists()) {
                    fname.mkdirs();
                }
                outFile = new FileOutputStream(destinationFile);
                // Setting buffer size to 8k. Apparently ideal.
                byte buf[] = new byte[8 * 1024];
                int len;
                while ((len = in.read(buf)) != -1) {
                    outFile.write(buf, 0, len);
                }
                logger.log(Level.INFO, "Writing completed for the file : {0}", destinationFile.replace("\\", "\\\\"));
            }
        } finally {
            try {
                if (outFile != null) {
                    outFile.close();
                }
            } catch (Exception ex) {
                logger.log(Level.WARNING, "Error occurred while closing output stream  {0}", ex);
            }
        }
    }

    /**
     * This method is used to return the string buffer by reading the data from input stream.
     * @param in : InputStream available from the download file location.
     * @return responseBuffer : string buffer by reading the data from input stream.
     */
    private String readDataBufferFromStream(InputStream in) throws IOException {
        String responseBuffer = "";
        StringBuilder sbuffer = new StringBuilder();
        byte buf[] = new byte[1024];
        int len;
        while ((len = in.read(buf)) != -1) {
            sbuffer.append(buf);
        }
        responseBuffer = sbuffer.toString();
        return responseBuffer;
    }

    /**
     * This class is used to convert the property data into NVPair Array which will be used to send the post data with http request.
     * @param formdata - This holds the key and value which needs to be sent in the http request.
     * @return post_form_data - returned the NameValue pair data in array.
     */
    private NVPair[] getPostFormData(Properties formdata) {
        NVPair[] post_form_data = null;
        if (formdata != null) {
            int size = formdata.size();
            if (size > 0) {
                int position = 0;
                post_form_data = new NVPair[size];
                Enumeration iterate = formdata.keys();
                while (iterate.hasMoreElements()) {
                    String key = (String) iterate.nextElement();
                    String value = (String) formdata.get(key);
                    post_form_data[position] = new NVPair(key, value);
                }
            }
        }
        return post_form_data;
    }
}

