javamanager/javainstaller/installer/javasrc/com/nokia/mj/impl/installer/downloader/Downloader.java
branchRCL_3
changeset 19 04becd199f91
child 23 98ccebc37403
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/javamanager/javainstaller/installer/javasrc/com/nokia/mj/impl/installer/downloader/Downloader.java	Tue Apr 27 16:30:29 2010 +0300
@@ -0,0 +1,560 @@
+/*
+* Copyright (c) 2008-2009 Nokia Corporation and/or its subsidiary(-ies).
+* All rights reserved.
+* This component and the accompanying materials are made available
+* under the terms of "Eclipse Public License v1.0"
+* which accompanies this distribution, and is available
+* at the URL "http://www.eclipse.org/legal/epl-v10.html".
+*
+* Initial Contributors:
+* Nokia Corporation - initial contribution.
+*
+* Contributors:
+*
+* Description:
+*
+*/
+
+
+package com.nokia.mj.impl.installer.downloader;
+
+import com.nokia.mj.impl.installer.Installer;
+import com.nokia.mj.impl.installer.utils.FileUtils;
+import com.nokia.mj.impl.installer.utils.InstallerException;
+import com.nokia.mj.impl.installer.utils.Log;
+import com.nokia.mj.impl.installer.utils.SysUtil;
+import com.nokia.mj.impl.rt.support.Jvm;
+import com.nokia.mj.impl.utils.Base64;
+import com.nokia.mj.impl.utils.InstallerErrorMessage;
+import com.nokia.mj.impl.utils.InstallerDetailedErrorMessage;
+import com.nokia.mj.impl.utils.OtaStatusCode;
+import com.nokia.mj.impl.utils.Tokenizer;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Downloader downloads file from specified URL.
+ * Each Downloader instance downloads one file at a time in
+ * a separate thread. Parallel downloads are possible with
+ * multiple Downloader instances.
+ *
+ * @author Nokia Corporation
+ * @version $Rev: 0 $ $Date$
+ */
+abstract public class Downloader implements Runnable
+{
+    // Maximum number of HTTP redirects to be followed.
+    protected static final int MAX_REDIRECT_COUNT = 5;
+
+    // Internal downloading states.
+    protected static final int STATE_DOWNLOADING = 1;
+    protected static final int STATE_STOPPED = 2;
+
+    // Internal downloading state.
+    protected int iState = 0;
+    // Download info.
+    protected DownloadInfo iDlInfo = null;
+    // Download listener.
+    protected DownloadListener iDlListener = null;
+    // Internet access point id.
+    protected String iIap = null;
+    // Service network access point id.
+    protected String iSnap = null;
+    // HTTP redirect count.
+    protected int iRedirectCount = 0;
+    // HTTP Basic authentication header value
+    protected String iAuthorizationHeader = null;
+    // OutpustStream for the downloaded file.
+    private OutputStream iOut = null;
+
+    /** Constructor. */
+    protected Downloader()
+    {
+    }
+
+    /** Constructor. */
+    protected Downloader(DownloadListener aDownloadListener,
+                         String aIap, String aSnap)
+    {
+        if (aDownloadListener == null)
+        {
+            InstallerException.internalError("Downloader: no listener specified");
+        }
+        iDlListener = aDownloadListener;
+        iIap = aIap;
+        iSnap = aSnap;
+    }
+
+    /**
+     * Start a new download.
+     *
+     * @param aDlInfo DownloadInfo object containing download URL and
+     * filename to which download is made.
+     */
+    synchronized public void start(DownloadInfo aDlInfo)
+    {
+        if (aDlInfo == null)
+        {
+            InstallerException.internalError
+            ("Downloader.start: no download info specified");
+        }
+        if (iDlInfo != null)
+        {
+            InstallerException.internalError
+            ("Downloader.start: downloader is already in use");
+        }
+        iDlInfo = aDlInfo;
+        iState = STATE_DOWNLOADING;
+        Thread thread = new Thread(this, "DownloaderThread");
+        Jvm.setThreadAsDaemon(thread, true);
+        thread.start();
+    }
+
+    /**
+     * Stop download.
+     */
+    synchronized public void stop()
+    {
+        if (iDlInfo == null)
+        {
+            return;
+        }
+        if (iState == STATE_DOWNLOADING)
+        {
+            iState = STATE_STOPPED;
+            try
+            {
+                if (iOut != null)
+                {
+                    // Close OutputStream.
+                    iOut.close();
+                    iOut = null;
+                }
+            }
+            catch (Throwable t)
+            {
+                Log.logError("Downloader.stop: closing OutputStream failed", t);
+            }
+            // Notify waitForCompletion() that download has been stopped.
+            this.notify();
+        }
+    }
+
+    /**
+     * This method blocks until download has been completed.
+     * If download is not going on, this method returns immediately.
+     */
+    synchronized public void waitForCompletion()
+    {
+        if (iDlInfo == null || iState != STATE_DOWNLOADING)
+        {
+            return;
+        }
+        try
+        {
+            this.wait();
+        }
+        catch (InterruptedException ie)
+        {
+            // Ignore
+        }
+    }
+
+    /**
+     * This method performs the actual download and is executed
+     * in its own Thread.
+     */
+    public void run()
+    {
+        Log.log("Downloader: download thread starts");
+        if (iDlInfo == null)
+        {
+            Log.logWarning("Downloader: no DownloadInfo, download thread exits");
+            return;
+        }
+
+        iDlInfo.setStartTime(System.currentTimeMillis());
+        try
+        {
+            // Notify listener that download has started.
+            iDlListener.started(iDlInfo);
+        }
+        catch (Exception ex)
+        {
+            // Ignore
+        }
+
+        try
+        {
+            // Open file for writing.
+            iOut = FileUtils.getOutputStream(iDlInfo.getFilename());
+            // Do the actual download.
+            doDownload(iOut);
+        }
+        catch (InstallerException iex)
+        {
+            Log.logError("Downloader: download failed", iex);
+            iDlInfo.setException(iex);
+        }
+        catch (Throwable t)
+        {
+            Log.logError("Downloader: download failed", t);
+            iDlInfo.setException(new InstallerException
+                                 (InstallerErrorMessage.INST_NO_NET, null,
+                                  InstallerDetailedErrorMessage.NO_NET,
+                                  new String[] { iDlInfo.getUrl() },
+                                  OtaStatusCode.LOSS_OF_SERVICE, t));
+        }
+        finally
+        {
+            try
+            {
+                if (iOut != null)
+                {
+                    // Close OutputStream.
+                    iOut.close();
+                    iOut = null;
+                }
+            }
+            catch (Throwable t)
+            {
+                Log.logError("Downloader: closing OutputStream failed", t);
+            }
+        }
+
+        iDlInfo.setEndTime(System.currentTimeMillis());
+        try
+        {
+            // Notify listener that download has ended.
+            iDlListener.ended(iDlInfo);
+        }
+        catch (Exception ex)
+        {
+            // Ignore
+        }
+        Log.log("Downloader: download thread exits");
+
+        // Notify that waitForCompletion() can proceed.
+        synchronized (this)
+        {
+            iState = STATE_STOPPED;
+            iDlInfo = null;
+            this.notify();
+        }
+    }
+
+    /**
+     * Returns true if given URL is downloadable, false otherwise.
+     * URL is downloadable if it has "http://" or "https://"
+     * prefix.
+     */
+    public static boolean isDownloadUrl(String aUrl)
+    {
+        if (aUrl == null || aUrl.length() == 0)
+        {
+            return false;
+        }
+        String lcUrl = aUrl.toLowerCase();
+        return (lcUrl.startsWith("http://") || lcUrl.startsWith("https://"));
+    }
+
+    /**
+     * Returns Base64 encoded string used in HTTP Basic authentication from
+     * username and password.
+     */
+    static String getBasicAuthBase64(String aUsername, String aPassword)
+    {
+        String basicAuthStr = aUsername + ":" + aPassword;
+        String base64Str = null;
+        try
+        {
+            // RFC2617 does not specify which character encoding should be
+            // used in strings used in basic authentication. Let's assume
+            // ISO-8859-1 which is default in RFC2616.
+            base64Str = Base64.encode(basicAuthStr.getBytes("ISO8859_1"));
+        }
+        catch (UnsupportedEncodingException uee)
+        {
+            InstallerException.internalError
+            ("Base64 encoding for Basic HTTP authentication failed", uee);
+        }
+        return base64Str;
+    }
+
+    /**
+     * Do the actual download and write downloaded data to specified
+     * OutputStream. This method must be implemented in
+     * Downloader subclass.
+     *
+     * @param aOut OutputStream to which downloaded data is written.
+     */
+    abstract protected void doDownload(OutputStream aOut) throws IOException;
+
+    /**
+     * Do the download from specified InputStream to specified OutputStream.
+     *
+     * @param aIn InputStream from which downloaded data is read.
+     * @param aOut OutputStream to which downloaded data is written.
+     */
+    protected void doDownload(InputStream aIn, OutputStream aOut)
+    throws IOException
+    {
+        // Size of data chunks read from net.
+        int netReadLen = 512;
+        // Amount of data read in between UI progress updates.
+        long progressUpdateLen = 4096;
+        if (iDlInfo.getTotalSize() > 0)
+        {
+            progressUpdateLen = iDlInfo.getTotalSize() / 20;
+        }
+        // Size of data chunks written do disk.
+        int diskWriteLen = 32768;
+        // Buffer for reading data.
+        byte[] buf = new byte[diskWriteLen];
+        int bufOffset = 0; // write offset to buf
+        int readCount = 0; // read count from is.read()
+        long lastProgressUpdate = 0; // when was progress updated
+        while (iState == STATE_DOWNLOADING && readCount >= 0)
+        {
+            int bufReadLen = buf.length - bufOffset;
+            if (bufReadLen > netReadLen)
+            {
+                bufReadLen = netReadLen;
+            }
+            readCount = aIn.read(buf, bufOffset, bufReadLen);
+            if (readCount > 0)
+            {
+                // Successfully read data, let's update counters.
+                bufOffset += readCount;
+                iDlInfo.addCurrentSize(readCount);
+                // Check that download size is withing expected range.
+                checkDownloadSize();
+                // Check if it is time to write data.
+                if (bufOffset == buf.length)
+                {
+                    // Check that there is enough free disk space.
+                    checkDiskSpace();
+                    // Write data from buf to output stream.
+                    aOut.write(buf, 0, bufOffset);
+                    bufOffset = 0;
+                }
+                // Check if it is time to do UI progress update.
+                if (iDlInfo.getCurrentSize() - lastProgressUpdate
+                        > progressUpdateLen)
+                {
+                    lastProgressUpdate = iDlInfo.getCurrentSize();
+                    try
+                    {
+                        // Notify listener that download has progressed.
+                        iDlListener.updateProgress(iDlInfo);
+                    }
+                    catch (Exception ex)
+                    {
+                        // Ignore
+                    }
+                }
+            }
+        }
+        if (bufOffset > 0)
+        {
+            // Check that there is enough free disk space.
+            checkDiskSpace();
+            // Write remaining bufOffset amount of data from buf
+            // to output stream.
+            aOut.write(buf, 0, bufOffset);
+            bufOffset = 0;
+        }
+        if (lastProgressUpdate < iDlInfo.getCurrentSize())
+        {
+            try
+            {
+                // Make final download progress update.
+                iDlListener.updateProgress(iDlInfo);
+            }
+            catch (Exception ex)
+            {
+                // Ignore
+            }
+        }
+    }
+
+    /**
+     * Checks if HTTP redirect is needed. If redirect is needed,
+     * this method updates new URL to iDlInfo and increases
+     * redirect counter.
+     *
+     * @param aHttpStatus HTTP response status code
+     * @param aLocation Location header from HTTP response
+     * @return true if redirect is needed, false otherwise
+     */
+    protected boolean redirectNeeded(int aHttpStatus, String aLocation)
+    {
+        boolean result = false;
+        if (iRedirectCount >= MAX_REDIRECT_COUNT)
+        {
+            // Maximum number of redirections has been exceeded.
+            return result;
+        }
+        if (aHttpStatus >= 301 && aHttpStatus <= 307 &&
+                isDownloadUrl(aLocation))
+        {
+            // Redirect is needed.
+            iDlInfo.setUrl(aLocation);
+            iRedirectCount++;
+            result = true;
+        }
+        return result;
+    }
+
+    /**
+     * Checks if HTTP Basic authentication is needed.
+     * If HTTP Basic authentication is needed,
+     * this method updates Authorization header
+     * value to iAuthorization member.
+     *
+     * @param aHttpStatus HTTP response status code
+     * @param aAuthenticate Authenticate header from HTTP response
+     * @return true if HTTP Basic authentication is needed, false otherwise
+     */
+    protected boolean basicAuthNeeded(int aHttpStatus, String aAuthenticate)
+    {
+        boolean result = false;
+        if (iAuthorizationHeader != null)
+        {
+            // Authorization header has already been set, which means
+            // that authentication has been tried and it failed.
+            // Do not try again.
+            return result;
+        }
+        if (aAuthenticate == null || aAuthenticate.length() == 0 ||
+                !aAuthenticate.trim().startsWith("Basic"))
+        {
+            return result;
+        }
+        if (aHttpStatus == 401 || aHttpStatus == 407)
+        {
+            iAuthorizationHeader = getAuthorizationHeaderValue();
+            result = true;
+        }
+        return result;
+    }
+
+    /**
+     * Asks username and password from DownloadListener
+     * and creates HTTP Authorization header used in HTTP
+     * Basic authentication.
+     *
+     * @return HTTP Authorization header value, or null
+     * if username and password could not be obtained
+     */
+    synchronized protected String getAuthorizationHeaderValue()
+    {
+        String[] usernamePassword =
+            iDlListener.getUsernamePassword(iDlInfo.getUrl());
+        if (usernamePassword == null)
+        {
+            // If username and password are not available,
+            // download cannot proceed.
+            Log.log("Downloader: username and password for HTTP authentication not available");
+            iState = STATE_STOPPED;
+            Installer.cancel();
+            return null;
+        }
+        return "Basic " + getBasicAuthBase64
+               (usernamePassword[0], usernamePassword[1]);
+    }
+
+    /**
+     * Parses HTTP status code from HTTP response status line.
+     *
+     * @param aStatusLine HTTP status line
+     * @return HTTP status code
+     */
+    protected static int getStatusCode(String aStatusLine)
+    {
+        if (aStatusLine == null || aStatusLine.length() == 0)
+        {
+            return 0;
+        }
+        String[] tokens = Tokenizer.split(aStatusLine, " ");
+        if (tokens == null || tokens.length < 2)
+        {
+            Log.logError("Downloader.getStatusCode: invalid HTTP Status-Line: " +
+                         aStatusLine);
+            return 0;
+        }
+        return Integer.parseInt(tokens[1]);
+    }
+
+    /**
+     * Checks that download size does not exceed the
+     * expected download size. Throws InstallerException
+     * if the expected size is exceeded.
+     */
+    protected void checkDownloadSize()
+    {
+        if (iDlInfo.isDrmContent())
+        {
+            // If the content is DRM protected its length can
+            // exceed the expected value.
+            return;
+        }
+        long expectedSize = iDlInfo.getExpectedSize();
+        if (expectedSize > 0)
+        {
+            boolean incorrectSize = false;
+            if (iDlInfo.getCurrentSize() > expectedSize)
+            {
+                incorrectSize = true;
+                Log.logError
+                ("Downloader.checkDownloadSize: download size (" +
+                 iDlInfo.getCurrentSize() +
+                 ") exceeds the expected size (" + expectedSize + ")");
+            }
+            if (!incorrectSize && iDlInfo.getTotalSize() > 0 &&
+                    iDlInfo.getTotalSize() != expectedSize)
+            {
+                incorrectSize = true;
+                Log.logError
+                ("Downloader.checkDownloadSize: download total size (" +
+                 iDlInfo.getTotalSize() +
+                 ") differs from the expected size (" + expectedSize + ")");
+            }
+            if (incorrectSize)
+            {
+                throw new InstallerException
+                (InstallerErrorMessage.INST_NO_NET, null,
+                 InstallerDetailedErrorMessage.ATTR_HANDLING_FAILED,
+                 new String[] { "MIDlet-Jar-Size" },
+                 OtaStatusCode.JAR_SIZE_MISMATCH);
+            }
+        }
+    }
+
+    /**
+     * Checks that there is enough free disk space for the
+     * remaining download. Throws appropriate InstallerException
+     * if there is not enough free disk space.
+     */
+    protected void checkDiskSpace()
+    {
+        int remainingSize =
+            (int)(iDlInfo.getExpectedSize() - iDlInfo.getCurrentSize());
+        if (remainingSize > 0)
+        {
+            if (SysUtil.isDiskSpaceBelowCriticalLevel(
+                        remainingSize, iDlInfo.getDrive()))
+            {
+                Log.logError("Disk space below critical level " +
+                             "during download, required space " +
+                             remainingSize + " bytes, drive " +
+                             FileUtils.getDriveName(iDlInfo.getDrive()));
+                throw InstallerException.getOutOfDiskSpaceException(
+                    remainingSize, null);
+            }
+        }
+    }
+}