Project: egit-github
/*******************************************************************************
 *  Copyright (c) 2011 GitHub Inc. 
 *  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.eclipse.org/legal/epl-v10.html 
 * 
 *  Contributors: 
 *    Kevin Sawicki (GitHub Inc.) - initial API and implementation 
 *******************************************************************************/
package org.eclipse.mylyn.internal.github.ui; 
 
import java.io.ByteArrayInputStream; 
import java.io.ByteArrayOutputStream; 
import java.io.IOException; 
import java.io.InputStream; 
import java.io.Serializable; 
import java.net.URL; 
import java.net.URLConnection; 
import java.text.MessageFormat; 
import java.util.HashMap; 
import java.util.Map; 
 
import org.eclipse.core.runtime.IProgressMonitor; 
import org.eclipse.core.runtime.IStatus; 
import org.eclipse.core.runtime.Status; 
import org.eclipse.core.runtime.jobs.ISchedulingRule; 
import org.eclipse.core.runtime.jobs.Job; 
import org.eclipse.swt.SWT; 
import org.eclipse.swt.graphics.GC; 
import org.eclipse.swt.graphics.Image; 
import org.eclipse.swt.graphics.ImageData; 
import org.eclipse.swt.graphics.ImageLoader; 
import org.eclipse.swt.graphics.Rectangle; 
import org.eclipse.swt.widgets.Display; 
import org.eclipse.ui.PlatformUI; 
 
/**
 * Stores Avatars 
 */
 
public class AvatarStore implements Serializable, ISchedulingRule { 
 
 /**
  * Callback interface for avatar image data loaded 
  */
 
 public interface IAvatarCallback { 
 
  /**
   * Avatar loaded 
   *  
   * @param data 
   * @param store 
   */
 
  void loaded(ImageData data, AvatarStore store); 
 
 
 
 /**
  * serialVersionUID 
  */
 
 private static final long serialVersionUID = -7791322302733784441L
 
 /**
  * TIMEOUT 
  */
 
 public static final int TIMEOUT = 30 * 1000
 
 /**
  * BUFFER_SIZE 
  */
 
 public static final int BUFFER_SIZE = 8192
 
 private Map<String, byte[]> avatars = new HashMap<String, byte[]>(); 
 
 /**
  * Get cached avatar image data 
  *  
  * @param url 
  * @return image data or null if not present in store 
  */
 
 public ImageData getAvatar(String url) { 
  ImageData data = null
  url = normalize(url); 
  if (url != null) { 
   byte[] cached = this.avatars.get(url); 
   if (cached != null
    data = getData(cached); 
  
  return data; 
 
 
 /**
  * Normalize url removing query parameters 
  *  
  * @param url 
  * @return normalized 
  */
 
 protected String normalize(String url) { 
  try { 
   URL parsed = new URL(url); 
   parsed = new URL(parsed.getProtocol(), parsed.getHost(), 
     parsed.getPath()); 
   return parsed.toExternalForm(); 
  catch (IOException e) { 
   return null
  
 
 
 /**
  * Load avatar 
  *  
  * @param url 
  * @param callback 
  */
 
 public void loadAvatar(final String url, final IAvatarCallback callback) { 
  Job job = new Job(MessageFormat.format("Loading avatar for {0}", url)) { 
 
   protected IStatus run(IProgressMonitor monitor) { 
    try { 
     ImageData data = loadAvatar(url); 
     if (data != null
      callback.loaded(data, AvatarStore.this); 
    catch (IOException ignore) { 
     // Ignored 
    
    return Status.OK_STATUS; 
   
  }; 
  job.schedule(); 
 
 
 /**
  * Load avatar image data from url 
  *  
  * @param url 
  * @return avatar image data 
  * @throws IOException 
  */
 
 public ImageData loadAvatar(String url) throws IOException { 
  url = normalize(url); 
  URL parsed = new URL(url); 
 
  byte[] data = this.avatars.get(url); 
  if (data != null
   return getData(data); 
 
  URLConnection connection = parsed.openConnection(); 
  connection.setConnectTimeout(TIMEOUT); 
 
  ByteArrayOutputStream output = new ByteArrayOutputStream(); 
  InputStream input = connection.getInputStream(); 
  try { 
   byte[] buffer = new byte[BUFFER_SIZE]; 
   int read = input.read(buffer); 
   while (read != -1) { 
    output.write(buffer, 0, read); 
    read = input.read(buffer); 
   
  finally { 
   try { 
    input.close(); 
   catch (IOException ignore) { 
    // Ignored 
   
   try { 
    output.close(); 
   catch (IOException ignore) { 
    // Ignored 
   
  
  data = output.toByteArray(); 
  this.avatars.put(url, data); 
  return getData(data); 
 
 
 /**
  * Get scaled image 
  *  
  * @param size 
  * @param data 
  * @return image data scaled to specified size 
  */
 
 public Image getScaledImage(int size, ImageData data) { 
  Display display = PlatformUI.getWorkbench().getDisplay(); 
  Image image = new Image(display, data); 
  Rectangle sourceBounds = image.getBounds(); 
 
  // Return original image and don't scale if size matches request 
  if (sourceBounds.width == size) 
   return image; 
 
  Image scaled = new Image(display, size, size); 
  GC gc = new GC(scaled); 
  try { 
   gc.setAntialias(SWT.ON); 
   gc.setInterpolation(SWT.HIGH); 
   Rectangle targetBounds = scaled.getBounds(); 
   gc.drawImage(image, 00, sourceBounds.width, sourceBounds.height, 
     00, targetBounds.width, targetBounds.height); 
  finally { 
   gc.dispose(); 
   image.dispose(); 
  
  return scaled; 
 
 
 /**
  * Get avatar image data 
  *  
  * @param bytes 
  * @return image data 
  */
 
 public ImageData getData(byte[] bytes) { 
  ByteArrayInputStream stream = new ByteArrayInputStream(bytes); 
  try { 
   ImageData[] images = new ImageLoader().load(stream); 
   if (images.length > 0
    return images[0]; 
  finally { 
   try { 
    stream.close(); 
   catch (IOException ignore) { 
    // Ignored 
   
  
  return null
 
 
 /**
  * @see org.eclipse.core.runtime.jobs.ISchedulingRule#contains(org.eclipse.core.runtime.jobs.ISchedulingRule) 
  */
 
 public boolean contains(ISchedulingRule rule) { 
  return this == rule; 
 
 
 /**
  * @see org.eclipse.core.runtime.jobs.ISchedulingRule#isConflicting(org.eclipse.core.runtime.jobs.ISchedulingRule) 
  */
 
 public boolean isConflicting(ISchedulingRule rule) { 
  return this == rule; 
 
}