Package org.rssowl.ui.internal.services

Source Code of org.rssowl.ui.internal.services.DownloadService$DownloadRequest

/*   **********************************************************************  **
**   Copyright notice                                                       **
**                                                                          **
**   (c) 2005-2009 RSSOwl Development Team                                  **
**   http://www.rssowl.org/                                                 **
**                                                                          **
**   All rights reserved                                                    **
**                                                                          **
**   This program and the accompanying materials are made available under   **
**   the terms of the Eclipse Public License v1.0 which accompanies this    **
**   distribution, and is available at:                                     **
**   http://www.rssowl.org/legal/epl-v10.html                               **
**                                                                          **
**   A copy is found in the file epl-v10.html and important notices to the  **
**   license from the team is found in the textfile LICENSE.txt distributed **
**   in this package.                                                       **
**                                                                          **
**   This copyright notice MUST APPEAR in all copies of the file!           **
**                                                                          **
**   Contributors:                                                          **
**     RSSOwl Development Team - initial API and implementation             **
**                                                                          **
**  **********************************************************************  */

package org.rssowl.ui.internal.services;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.window.Window;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.program.Program;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.progress.IProgressConstants;
import org.rssowl.core.Owl;
import org.rssowl.core.connection.AuthenticationRequiredException;
import org.rssowl.core.connection.ConnectionException;
import org.rssowl.core.connection.CredentialsException;
import org.rssowl.core.connection.HttpConnectionInputStream;
import org.rssowl.core.connection.IAbortable;
import org.rssowl.core.connection.IConnectionPropertyConstants;
import org.rssowl.core.connection.IProtocolHandler;
import org.rssowl.core.internal.InternalOwl;
import org.rssowl.core.internal.persist.pref.DefaultPreferences;
import org.rssowl.core.persist.IAttachment;
import org.rssowl.core.persist.INews;
import org.rssowl.core.persist.pref.IPreferenceScope;
import org.rssowl.core.util.CoreUtils;
import org.rssowl.core.util.DateUtils;
import org.rssowl.core.util.StreamGobbler;
import org.rssowl.core.util.StringUtils;
import org.rssowl.core.util.URIUtils;
import org.rssowl.ui.internal.Activator;
import org.rssowl.ui.internal.Controller;
import org.rssowl.ui.internal.OwlUI;
import org.rssowl.ui.internal.dialogs.LoginDialog;
import org.rssowl.ui.internal.util.DownloadJobQueue;
import org.rssowl.ui.internal.util.JobRunner;

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.net.URI;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
* A service to download files in a {@link DownloadJobQueue} with proper
* progress reporting.
*
* @author bpasero
*/
public class DownloadService {

  /* Max. number of concurrent Jobs for downloading files */
  private static final int MAX_CONCURRENT_DOWNLOAD_JOBS = 3;

  /* Connection Timeouts in MS */
  private static final int DEFAULT_CON_TIMEOUT = 30000;

  /* Default Length for Download Tasks */
  private static final int DEFAULT_TASK_LENGTH = 1000000;

  /* Default Progress for Download Tasks */
  private static final int DEFAULT_WORKED = 200;

  /* List of invalid characters for a file name */
  private static final List<Character> INVALID_FILENAME_CHAR = Arrays.asList('\\', '/', ':', '?', '|', '*', '<', '>', '\"');

  /* A simple date format used to produce unique download names if necessary */
  private static final SimpleDateFormat DOWNLOAD_FILE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd-HHmm", Locale.US); //$NON-NLS-1$

  /* A suffix for download parts */
  private static final String DOWNLOAD_PART_SUFFIX = ".part"; //$NON-NLS-1$

  /* Filename portion of content disposition header */
  private static final String CONTENT_DISPOSITION_FILENAME = "filename="; //$NON-NLS-1$

  /* Some Content Types that identify a HTML content */
  private static final List<String> HTML_CONTENT_TYPES = Arrays.asList(new String[] { "text/html", "application/xhtml+xml" }); //$NON-NLS-1$ //$NON-NLS-2$

  private DownloadJobQueue fDownloadQueue;
  private Map<OutputStream, OutputStream> fOutputStreamMap = new ConcurrentHashMap<OutputStream, OutputStream>();
  private IPreferenceScope fPreferences = Owl.getPreferenceService().getGlobalScope();

  /* Task for a Download */
  private class AttachmentDownloadTask extends DownloadJobQueue.DownloadTask {
    private final DownloadRequest fRequest;

    private AttachmentDownloadTask(DownloadRequest request) {
      fRequest = request;
    }

    @Override
    public IStatus run(Job job, IProgressMonitor monitor) {
      return internalDownload(fRequest, job, monitor);
    }

    public String getName() {
      return NLS.bind(Messages.DownloadService_DOWNLOADING_N, fRequest.getLink().toString());
    }

    public Priority getPriority() {
      return Priority.DEFAULT;
    }

    @Override
    public int hashCode() {
      return fRequest.getLink().hashCode();
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj)
        return true;

      if (obj == null)
        return false;

      if (getClass() != obj.getClass())
        return false;

      final AttachmentDownloadTask other = (AttachmentDownloadTask) obj;
      return fRequest.getLink().equals(other.fRequest.getLink());
    }
  }

  /* A download request to process by the service */
  public static class DownloadRequest {
    private final URI fLink;
    private final File fTargetFolder;
    private final IAttachment fAttachment;
    private final INews fNews;
    private final boolean fIsUserInitiated;
    private final String fUserChosenFilename;

    /* Download an Attachment */
    public static DownloadRequest createAttachmentDownloadRequest(IAttachment attachment, URI link, File targetFolder, boolean isUserInitiated, String userChosenFilename) {
      return new DownloadRequest(link, targetFolder, attachment, null, isUserInitiated, userChosenFilename);
    }

    /* Download the content that is behind the News Link if any */
    public static DownloadRequest createNewsDownloadRequest(INews news, URI link, File targetFolder) {
      return new DownloadRequest(link, targetFolder, null, news, false, null);
    }

    private DownloadRequest(URI link, File targetFolder, IAttachment attachment, INews news, boolean isUserInitiated, String userChosenFilename) {
      fLink = link;
      fTargetFolder = targetFolder;
      fAttachment = attachment;
      fNews = news;
      fIsUserInitiated = isUserInitiated;
      fUserChosenFilename = userChosenFilename;
    }

    URI getLink() {
      return fLink;
    }

    File getTargetFolder() {
      return fTargetFolder;
    }

    IAttachment getAttachment() {
      return fAttachment;
    }

    INews getNews() {
      return fNews;
    }

    String getUserChosenFilename() {
      return fUserChosenFilename;
    }

    boolean isUserInitiated() {
      return fIsUserInitiated;
    }

    String getType() {
      return fAttachment != null ? fAttachment.getType() : null;
    }

    int getLength() {
      return fAttachment != null ? fAttachment.getLength() : 0;
    }

    boolean isAttachmentDownloadRequest() {
      return fAttachment != null;
    }

    boolean isNewsDownloadRequest() {
      return fNews != null;
    }
  }

  /** Default Constructor to create a Download Queue */
  public DownloadService() {
    fDownloadQueue = new DownloadJobQueue(Messages.DownloadService_DOWNLOADING_TITLE, MAX_CONCURRENT_DOWNLOAD_JOBS, Integer.MAX_VALUE);
  }

  /**
   * @param download the requested file to download from the service.
   */
  public void download(DownloadRequest download) {
    AttachmentDownloadTask task = new AttachmentDownloadTask(download);
    if (InternalOwl.TESTING) //Support to test the download service from JUnit
      internalDownload(download, new StreamGobbler(null), new NullProgressMonitor());
    else if (!fDownloadQueue.isQueued(task))
      fDownloadQueue.schedule(task);
  }

  private IStatus internalDownload(final DownloadRequest request, Job job, final IProgressMonitor monitor) {

    /* Do not download in Offline Mode */
    if (Controller.getDefault().isOffline())
      return Status.OK_STATUS;

    /* Find Download Name */
    String downloadFileName;
    if (StringUtils.isSet(request.getUserChosenFilename()))
      downloadFileName = request.getUserChosenFilename();
    else
      downloadFileName = URIUtils.getFile(request.fLink, OwlUI.getExtensionForMime(request.getType()));

    job.setProperty(IProgressConstants.ICON_PROPERTY, OwlUI.getAttachmentImage(downloadFileName, request.getType()));

    int bytesConsumed = 0;
    try {
      IProtocolHandler handler = Owl.getConnectionService().getHandler(request.getLink());
      if (handler != null) {
        Map<Object, Object> properties = new HashMap<Object, Object>();
        properties.put(IConnectionPropertyConstants.CON_TIMEOUT, DEFAULT_CON_TIMEOUT);

        /* Check for Cancellation and Shutdown */
        if (monitor.isCanceled() || Controller.getDefault().isShuttingDown())
          return Status.CANCEL_STATUS;

        /* Initialize Fields */
        long bytesPerSecond = 0;
        long lastTaskNameUpdate = 0;
        long lastBytesCheck = 0;
        int length = request.getLength();
        byte[] buffer = new byte[8192];

        /* First Download to a temporary File */
        int contentLength = length;
        InputStream in = null;
        FileOutputStream out = null;
        File partFile = null;
        boolean canceled = false;
        Exception error = null;
        try {

          /* Open Stream */
          in = handler.openStream(request.getLink(), monitor, properties);

          /* Obtain real Content Length from Stream if available */
          if (in instanceof HttpConnectionInputStream) {
            int len = ((HttpConnectionInputStream) in).getContentLength();
            if (len > 0)
              contentLength = len;
          }

          /* If we download a News Link, now is a good time to check for the Content Type making any sense */
          if (request.isNewsDownloadRequest() && in instanceof HttpConnectionInputStream) {
            String contentType = ((HttpConnectionInputStream) in).getContentType();
            if (isTextualContent(contentType)) {
              canceled = true;
              return Status.CANCEL_STATUS;
            }
          }

          /* Begin Task (now because the real content length is known at this point) */
          job.setName(NLS.bind(Messages.DownloadService_DOWNLOADING, downloadFileName));
          monitor.beginTask(formatTask(bytesConsumed, contentLength, -1), contentLength > 0 ? contentLength : DEFAULT_TASK_LENGTH);

          /* Create tmp part File */
          partFile = getPartFile(request.getTargetFolder(), downloadFileName);

          /* Maybe the chosen directory is not writeable */
          if (partFile == null) {
            canceled = true;
            return Status.CANCEL_STATUS;
          }

          /* Keep Outputstream for later */
          out = new FileOutputStream(partFile);
          fOutputStreamMap.put(out, out);

          /* Download */
          while (true) {

            /* Check for Cancellation and Shutdown */
            if (monitor.isCanceled() || Controller.getDefault().isShuttingDown()) {
              canceled = true;
              return Status.CANCEL_STATUS;
            }

            /* Read from Stream */
            int read = in.read(buffer);
            bytesConsumed += read;
            if (read == -1)
              break;

            /* Write to File */
            out.write(buffer, 0, read);

            /* Update Task Name once per Second */
            long now = System.currentTimeMillis();
            long timeDiff = (now - lastTaskNameUpdate);
            if (timeDiff > 1000) {
              long bytesDiff = bytesConsumed - lastBytesCheck;
              bytesPerSecond = bytesDiff / (timeDiff / 1000);
              monitor.setTaskName(formatTask(bytesConsumed, contentLength, (int) bytesPerSecond));
              lastTaskNameUpdate = now;
              lastBytesCheck = bytesConsumed;
            }

            /* Report accurate progress */
            if (request.getLength() > 0)
              monitor.worked(read);

            /* Report calculated progress if possible */
            else if (contentLength > 0) {
              float relWorked = read / (float) contentLength;
              monitor.worked((int) (relWorked * DEFAULT_TASK_LENGTH));
            }

            /* Use a generic Progress Value */
            else
              monitor.worked(DEFAULT_WORKED);
          }
        } catch (FileNotFoundException e) {
          error = e;
          return Activator.getDefault().createErrorStatus(e.getMessage(), e);
        } catch (IOException e) {
          error = e;
          return Activator.getDefault().createErrorStatus(e.getMessage(), e);
        } catch (ConnectionException e) {
          final boolean showError[] = new boolean[] { true };

          /* Offer a Login Dialog if Authentication is Required */
          if (request.isUserInitiated() && e instanceof AuthenticationRequiredException && !monitor.isCanceled() && !Controller.getDefault().isShuttingDown()) {
            final Shell shell = OwlUI.getActiveShell();
            if (shell != null && !shell.isDisposed()) {
              Controller.getDefault().getLoginDialogLock().lock();
              try {
                final AuthenticationRequiredException authEx = (AuthenticationRequiredException) e;
                JobRunner.runSyncedInUIThread(shell, new Runnable() {
                  public void run() {

                    /* Return on Cancelation or shutdown or deletion */
                    if (monitor.isCanceled() || Controller.getDefault().isShuttingDown())
                      return;

                    /* Credentials might have been provided meanwhile in another dialog */
                    try {
                      URI normalizedUri = URIUtils.normalizeUri(request.getLink(), true);
                      if (Owl.getConnectionService().getAuthCredentials(normalizedUri, authEx.getRealm()) != null) {
                        fDownloadQueue.schedule(new AttachmentDownloadTask(request));
                        showError[0] = false;
                        return;
                      }
                    } catch (CredentialsException exe) {
                      Activator.getDefault().getLog().log(exe.getStatus());
                    }

                    /* Show Login Dialog */
                    LoginDialog login = new LoginDialog(shell, request.getLink(), authEx.getRealm());
                    if (login.open() == Window.OK && !monitor.isCanceled() && !Controller.getDefault().isShuttingDown()) {
                      fDownloadQueue.schedule(new AttachmentDownloadTask(request));
                      showError[0] = false;
                    }
                  }
                });
              } finally {
                Controller.getDefault().getLoginDialogLock().unlock();
              }
            }
          }

          /* User has not Provided Login Credentials or any other error */
          if (showError[0]) {
            error = e;
            return Activator.getDefault().createErrorStatus(e.getMessage(), e);
          }

          /* User has Provided Login Credentials - cancel this Task */
          monitor.setCanceled(true);
          canceled = true;
          return Status.CANCEL_STATUS;
        } finally {
          monitor.done();

          /* Indicate Error Message if any and offer Action to download again */
          if (error != null) {
            String errorMessage = CoreUtils.toMessage(error);
            if (StringUtils.isSet(errorMessage))
              job.setName(NLS.bind(Messages.DownloadService_ERROR_DOWNLOADING_N, downloadFileName, errorMessage));
            else
              job.setName(NLS.bind(Messages.DownloadService_ERROR_DOWNLOADING, downloadFileName));

            job.setProperty(IProgressConstants.ICON_PROPERTY, OwlUI.ERROR);

            DownloadRequest redownloadRequest = new DownloadRequest(request.getLink(), request.getTargetFolder(), request.getAttachment(), request.getNews(), true, request.getUserChosenFilename());
            job.setProperty(IProgressConstants.ACTION_PROPERTY, getRedownloadAction(new AttachmentDownloadTask(redownloadRequest)));
            monitor.setTaskName(Messages.DownloadService_TRY_AGAIN);
          }

          /* Close Output Stream */
          if (out != null) {
            try {
              out.close();
              fOutputStreamMap.remove(out);
              if (partFile != null && (canceled || error != null))
                partFile.delete();
            } catch (IOException e) {
              return Activator.getDefault().createErrorStatus(e.getMessage(), e);
            }
          }

          /* Close Input Stream */
          if (in != null) {
            try {
              if ((canceled || error != null) && in instanceof IAbortable)
                ((IAbortable) in).abort();
              else
                in.close();
            } catch (IOException e) {
              return Activator.getDefault().createErrorStatus(e.getMessage(), e);
            }
          }
        }

        /* Check for Cancellation and Shutdown */
        if (monitor.isCanceled() || Controller.getDefault().isShuttingDown()) {
          if (partFile != null)
            partFile.delete();
          return Status.CANCEL_STATUS;
        }

        /* Something was not working right if the part file is null */
        if (partFile == null)
          return Status.CANCEL_STATUS;

        /* Now copy over the part file to the actual file in an atomic operation */
        String finalFileName;
        if (StringUtils.isSet(request.getUserChosenFilename()))
          finalFileName = request.getUserChosenFilename();
        else
          finalFileName = getDownloadFileName(request, in);

        File downloadFile = new File(request.getTargetFolder(), finalFileName);
        if (!partFile.renameTo(downloadFile)) {
          downloadFile.delete();
          partFile.renameTo(downloadFile);
        }

        /* Offer Action to Open Attachment by keeping Job in Viewer if set */
        if (!fPreferences.getBoolean(DefaultPreferences.HIDE_COMPLETED_DOWNLOADS)) {
          job.setProperty(IProgressConstants.KEEP_PROPERTY, Boolean.TRUE);
          job.setProperty(IProgressConstants.ACTION_PROPERTY, getOpenAction(downloadFile));
        }
      }
    } catch (ConnectionException e) {
      return Activator.getDefault().createErrorStatus(e.getMessage(), e);
    }

    /* Update Job Name */
    if (bytesConsumed > 0)
      job.setName(NLS.bind(Messages.DownloadService_N_OF_M, downloadFileName, OwlUI.getSize(bytesConsumed)));
    else
      job.setName(downloadFileName);

    /* The Label of the Status is used as Link for Action */
    return new Status(IStatus.OK, Activator.PLUGIN_ID, Messages.DownloadService_OPEN_FOLDER);
  }

  private boolean isTextualContent(String contentType) {
    if (StringUtils.isSet(contentType)) {
      for (String htmlContentType : HTML_CONTENT_TYPES) {
        if (contentType.contains(htmlContentType))
          return true;
      }
    }

    return false;
  }

  private File getPartFile(File targetFolder, String name) throws IOException {
    name = toValidFileName(name);
    File partFile = null;

    /* Up to 10 attempts to create a non existing file */
    for (int i = 0; i < 10; i++) {
      if (i == 0)
        partFile = new File(targetFolder, name + DOWNLOAD_PART_SUFFIX);
      else
        partFile = new File(targetFolder, name + "_" + i + DOWNLOAD_PART_SUFFIX); //$NON-NLS-1$

      if (!partFile.exists() && partFile.createNewFile())
        break;
    }

    if (partFile != null)
      partFile.deleteOnExit();

    return partFile;
  }

  private String toValidFileName(String fileName) {
    for (Character invalidChar : INVALID_FILENAME_CHAR) {
      fileName = fileName.replace(invalidChar, '_');
    }

    return fileName;
  }

  private String getDownloadFileName(DownloadRequest request, InputStream inS) {
    String downloadFileName = null;

    /* Try to read out the Content-Disposition header first */
    if (inS instanceof HttpConnectionInputStream && StringUtils.isSet(((HttpConnectionInputStream) inS).getContentDisposition())) {
      String contentDisposition = ((HttpConnectionInputStream) inS).getContentDisposition();

      int indexOfFileName = contentDisposition.indexOf(CONTENT_DISPOSITION_FILENAME);
      if (indexOfFileName != -1) {
        contentDisposition = contentDisposition.substring(indexOfFileName + CONTENT_DISPOSITION_FILENAME.length());
        contentDisposition = StringUtils.replaceAll(contentDisposition, "\"", ""); //$NON-NLS-1$ //$NON-NLS-2$
        downloadFileName = contentDisposition.trim();
      }
    }

    /* Otherwise retrieve a good name from the URI */
    if (!StringUtils.isSet(downloadFileName))
      downloadFileName = URIUtils.getFile(request.getLink(), OwlUI.getExtensionForMime(request.getType()));

    /* Make sure the file name is valid for the OS */
    downloadFileName = toValidFileName(downloadFileName);

    /* If the file already exists, add the news date as suffix to the file name */
    File proposedFile = new File(request.getTargetFolder(), downloadFileName);
    if (proposedFile.exists()) {
      INews news = request.getNews();
      if (news == null)
        news = request.getAttachment().getNews();

      Date date = DateUtils.getRecentDate(news);
      if (date != null) {
        String fileNameSuffix = DOWNLOAD_FILE_DATE_FORMAT.format(date);

        int index = downloadFileName.lastIndexOf('.');
        if (index == -1)
          downloadFileName += "_" + fileNameSuffix; //$NON-NLS-1$
        else {
          String pre = downloadFileName.substring(0, index);
          String post = downloadFileName.substring(index);

          downloadFileName = pre + "_" + fileNameSuffix + post; //$NON-NLS-1$
        }
      }
    }

    return downloadFileName;
  }

  private String formatTask(int bytesConsumed, int totalBytes, int bytesPerSecond) {
    StringBuilder str = new StringBuilder();

    /* "Time Remaining" */
    int bytesToGo = totalBytes - bytesConsumed;
    if (bytesToGo > 0 && bytesPerSecond > 0) {
      int secondsRemaining = bytesToGo / bytesPerSecond;
      String period = OwlUI.getPeriod(secondsRemaining);
      if (period != null)
        str.append(NLS.bind(Messages.DownloadService_BYTES_REMAINING, period)).append(" - "); //$NON-NLS-1$
    }

    /* "X MB of Y MB "*/
    String consumed = OwlUI.getSize(bytesConsumed);
    if (consumed == null)
      consumed = "0"; //$NON-NLS-1$

    String total = OwlUI.getSize(totalBytes);
    if (total != null)
      str.append(NLS.bind(Messages.DownloadService_BYTES_OF_BYTES, consumed, total));
    else
      str.append(NLS.bind(Messages.DownloadService_BYTES_OF_UNKNOWN, consumed));

    /* "(X MB/sec)" */
    if (bytesPerSecond > 0) {
      str.append(" "); //$NON-NLS-1$
      str.append(NLS.bind(Messages.DownloadService_BYTES_PER_SECOND, OwlUI.getSize(bytesPerSecond)));
    }

    return str.toString();
  }

  private IAction getOpenAction(final File downloadFile) {
    return new Action(Messages.DownloadService_OPEN_FOLDER) {
      @Override
      public void run() {
        Program.launch(downloadFile.getParent());
      }
    };
  }

  private IAction getRedownloadAction(final AttachmentDownloadTask task) {
    return new Action(Messages.DownloadService_RE_DOWNLOAD) {
      @Override
      public void run() {
        fDownloadQueue.schedule(task);
      }
    };
  }

  /**
   * Stops this Service and cancels all pending downloads.
   */
  public void stopService() {
    fDownloadQueue.cancel(false);

    /* Need to properly close yet opened Streams */
    Set<OutputStream> openStreams = fOutputStreamMap.keySet();
    for (OutputStream out : openStreams) {
      try {
        out.close();
      } catch (IOException e) {
        /* Ignore */}
    }
  }

  /**
   * @return <code>true</code> if there are active download jobs running and
   * <code>false</code> otherwise.
   */
  public boolean isActive() {
    return fDownloadQueue.isWorking();
  }
}
TOP

Related Classes of org.rssowl.ui.internal.services.DownloadService$DownloadRequest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.