Blog Posts - July 2014

The power of promises for file downloading

In this blog post I will be implementing a file download with a progress indicator using cookies, AngularJS and the promises.

Promises are a powerful concept with a number of advantages, in the following implementation pay attention to these points (your more then welcome to comment):

  1. Clarity and readability of code
  2. Error handling
  3. Separation of concerns

I thought of showing the same implementation without promises, but I think anyone who has tried to handle more than one callback and handle the error cases properly will easily see the difference.

The Module

A download button that changes it’s text with set intervals.
At the end it should be in a success state or an error state.
To complicate things a little and show the power of promises I added another step called “validateBeforeDownload”, this step will call the server to validate the download and fail it if necessary.

download (1) arrow-vector-2 aausv arrow-vector-2 download (5)
See It Live!

Downloading a file

The standard way of downloading a file is with a simple “a” tag with an href.
In order to do be able to add the “validateBeforeDownload” step and avoid passing “dom” to a service – I am using an Iframe which a service creates and destroys. This will trigger the download and if the server headers are appropriate the download will begin.

Service Code

var generateIframeDownload = function(){
  var iframe = document.createElement('iframe');
  $cookieStore.put('download_file', 'true');

  iframe.src = '/myserver/dowload';
  iframe.style.display = "none";
  document.body.appendChild(iframe);  
}

Adding in the progress

Easier said then done! Downloading a file can’t be done with an simple ajax call, so you can’t tell when the download is complete.
The solution I’m using is setting a cookie, let’s call it “download_file” with a timer that checks for a cookie every 500ms.

  • While the cookie exists the loading state is preserved.
  • Once the request completes, the server deletes the cookie and the timer is stopped.

This isn’t the best solution but is simple and doesn’t require sockets or external plugins.

Service Code

var manageIframeProgress = function(){
  var defer = $q.defer();
  // notify that the download is in progress every half a second / do this for a maximum of 50 intervals 
  var promise = $interval(function () {
      if (!$cookieStore.get('download_file')){
        $interval.cancel(promise);
      }
  }, 500, 50);
      
  promise.then(defer.reject, defer.resolve, defer.notify);
  
  promise.finally(function () {
    $cookieStore.remove('download_file');
    document.body.removeChild(iframe);
  });
}

Java Server

Just to get the full stack of implementation here is the code for handling the response data and the clearing of the cookie.

public String exportExcel() throws Exception {
 final byte[] bytesToOutput = createExcelReport().toByteArray();
 output = new ByteArrayInputStream(bytesToOutput);
 fileSize = bytesToOutput.length;
 HttpServletResponse response  = getResponse();
 Cookie cookie = new Cookie("download_file", "true");
 cookie.setPath("/");
 cookie.setMaxAge(0);
 cookie.setSecure(true);
 response.addCookie(cookie);
 return "exportCsv";
}

Wrapping everything together with promises

Pay attention to the comments in the code, some of the code is there to simulate the server requests and response and are only there for the full picture.

HTML

Each visual state of the button is determined by it’s text (scope.downloadExcelText).

Service

Notice $timeout mocks an asynchronous call and it’s response to a server.
this would normally be done with $http.

angular.module("fileDownload").factory("downloadService", function($interval, $timeout, $q, $cookieStore){
  
  var generateIframeDownload = function(){
    var iframe = document.createElement('iframe');
    $cookieStore.put('download_file', 'true');

    iframe.src = '/myserver/dowload';
    iframe.style.display = "none";
    document.body.appendChild(iframe);  
  }
  
  var manageIframeProgress = function(){
      var defer = $q.defer();
      
      // notify that the download is in progress every half a second / do this for a maximum of 50 intervals 
      var promise = $interval(function () {
        if (!$cookieStore.get('download_file')){
          $interval.cancel(promise);
        }
      }, 500, 50);
      
      promise.then(defer.reject, defer.resolve, defer.notify);
      
      promise.finally(function () {
        $cookieStore.remove('download_file');
        document.body.removeChild(iframe);
      });
  }
  
  return {
    validateBeforeDownload: function (config) {
      var defer = $q.defer();
      
      // notify that the download is in progress every half a second
      $interval(function (i) {
        defer.notify(i);
      }, 500);
    
      //mock response from server - this would typicaly be a $http request and response
      $timeout(function () {
        // in case of error: 
         //defer.reject("this file can not be dowloaded");
         defer.resolve(config);
      }, 2000);
  
      return defer.promise;
    },
    downloadFile: function (config) {
    
      generateIframeDownload();
      var promise = manageIframeProgress();
  
      //mock response from server - this would be automaticlly triggered by the file download compeletion
      $timeout(function(){
        $cookieStore.remove('download_file');
      }, 3000);
      
      return promise;
    }
  }
});

Controller

This is were our hard work pays off and promises start to shine.

Lets step into the promise mechanism –
Prepending the “downloadService.validateBeforeDownload” to the “downloadService.downloadExcel” with the “then” method creates a third promise which shares callbacks for: success, failure and notifications (for the progress).
There is also a finally callback attached to this promise that we use for sharing code between the success and failure.
But the really nice thing here is it also enables handling errors just from the “validateBeforeDownload”, and bubbling them up if needed with $q.reject or by simply throwing the error.

Pay attention that each step towards completion of the promise seems to be handled in an async manner and the actual asynchronicity is handled by the promise mechanism and the service. Magic!

angular.module("fileDownload").controller("downloadCtrl", function($scope, $timeout, $q, downloadService){
  $scope.downloadFile = function(){
    var params = {};
    var loadingText = 'Loading Data';
    var options = ['.', '..', '...'];
 
    $scope.downloadFileText = loadingText + options[0];
    var promise = downloadService.validateBeforeDownload(params).then(null, function (reason) {
      alert(reason);
      // you can also throw the error
      // throw reason;
      return $q.reject(reason);
    }).then(downloadService.downloadFile).then(function(){
      $scope.downloadFileText = 'Loaded';
    }, function(){
      $scope.downloadFileText = 'Failed';
    }, function(i){
      i = (i+1)%3;
      $scope.downloadFileText = loadingText + options[i];
    });
    
    promise.finally(function(){
      $timeout(function(){
        delete $scope.downloadFileText;  
      }, 2000);
    });
  };
});