Tuesday, December 25, 2012

Downloading Images with java NIO

So We got this flex application, it's got tons of images it's got to show and the client is not too happy with the loading time. It connects to a JEE back-end which pushes these image URLs. 

I have already cached requests at the back end, solved half the problem actually. And while we were at it, we cached the images at the front end too. Now, it takes around 6-7 seconds for a screen(state) to load the first time around, but we have managed to cut the subsequent requests down to 2-3 seconds range with request and content caching. The client, well he is still not too happy with initial loading times(no surprises there). So we came up with the idea to pre-download the images on to the local machine the application is running on.


Which brings us over to the topic of the post, downloading images. We wanted this to be light weight, so I initially planned on using Apache's HTTP client to do the dirty work asynchronously.Turns out it's slower than my arthritic granny, I couldn't have that. So I figured why not NIO, it's fast, it is efficient and gets the job done.


The download action is given below, I used an interface, but this is really not necessary, as I am not adding anything to it.

public interface Downloadable extends Runnable {
 
}

and here is the implementation of the download artifact,

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;

public class DownloadImage implements Downloadable {
 private String imageUrl;

 public DownloadImage(String imageUrl) {
  this.imageUrl = imageUrl;
 }

 public void run() {
  String resultFileName = DigestTools.getMD5Digest(imageUrl);
  try {
   URL url = new URL(imageUrl);
   ReadableByteChannel readableByteChannel = Channels.newChannel(url.openStream());
   FileOutputStream fileOutputStream = new FileOutputStream(
     PropertyLoader.getDownloadFileLocation() + resultFileName);
   FileChannel fileChannel = fileOutputStream.getChannel();
   fileChannel.transferFrom(readableByteChannel,0, 1 << 24);
   fileOutputStream.close();
   fileChannel.close();
   readableByteChannel.close();
  } catch (MalformedURLException e) {
   e.printStackTrace();
  }catch (IOException e) {
   e.printStackTrace();
  }
 }

 /*
  * (non-Javadoc)
  * 
  * @see java.lang.Object#hashCode()
  */
 @Override
 public int hashCode() {
  final int prime = 31;
  int result = 1;
  result = prime * result
    + ((imageUrl == null) ? 0 : imageUrl.hashCode());
  return result;
 }

 /*
  * (non-Javadoc)
  * 
  * @see java.lang.Object#equals(java.lang.Object)
  */
 @Override
 public boolean equals(Object obj) {
  if (this == obj) {
   return true;
  }
  if (obj == null) {
   return false;
  }
  if (!(obj instanceof DownloadImage)) {
   return false;
  }
  DownloadImage other = (DownloadImage) obj;
  if (imageUrl == null) {
   if (other.imageUrl != null) {
    return false;
   }
  } else if (!imageUrl.equals(other.imageUrl)) {
   return false;
  }
  return true;
 }

 @Override
 public String toString() {
  return "Download Artifact : " + imageUrl;
 }
}

Note that I have used several custom utility classes, like DigestTools and PropertyLoader. And now to the executor, I decided to go with Java Executor Service.

public class ImageDownloader {
 private static final Logger LOGGER = Logger
   .getLogger(ImageDownloader.class);
 private static final boolean DEBUG = LOGGER.isDebugEnabled();
 Collection<Downloadable> downloadables;
 ExecutorService executorService = Executors.newFixedThreadPool(PropertyLoader.getConcurrentThreads());

 public ImageDownloader() {
  if (DEBUG) {
   LOGGER.debug("Initializing image downloader");
  }
  downloadables = new HashSet<Downloadable>();
 }

 public void addDownloadable(Downloadable downloadable) {
  if (DEBUG) {
   LOGGER.debug("Adding downloadable with url " + downloadable);
  }
  downloadables.add(downloadable);
 }

 public void downloadImages() throws InterruptedException,
   ExecutionException, TimeoutException {
  if (executorService.isShutdown()) {
   executorService = Executors.newFixedThreadPool(PropertyLoader.getConcurrentThreads());
  }
  Collection<Future<Downloadable>> futures = new HashSet<Future<Downloadable>>();
  for (Downloadable downloadable : downloadables) {
   Future<Downloadable> future = executorService.submit(downloadable,downloadable);
   future.get(6, TimeUnit.SECONDS);
   futures.add(future);
  }
  for (Future<Downloadable> future : futures) {
   future.get();
  }
  executorService.shutdown();
  downloadables = Collections.EMPTY_SET;
 }
}

It's just a matter of firing it up now.



public class PartyStarter {

 /**
  * @param args
  * @throws ExecutionException 
  * @throws InterruptedException 
  * @throws TimeoutException 
  * @throws IOException 
  */
 public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException, IOException {
  Downloadable d1 = new DownloadImage("image_url1");
  Downloadable d2 = new DownloadImage("image_url2");
  
  ImageDownloader imageDownloader = new ImageDownloader();
  imageDownloader.addDownloadable(d1);
  imageDownloader.downloadImages();
 }

}
That's it for this post folks.

1 comment: