package org.multibit.hd.ui.gravatar;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService;
import org.joda.time.DateTime;
import org.multibit.commons.concurrent.SafeExecutors;
import org.multibit.commons.utils.Dates;
import org.multibit.hd.core.dto.RAGStatus;
import org.multibit.hd.ui.MultiBitUI;
import org.multibit.hd.ui.events.controller.ControllerEvents;
import org.multibit.hd.ui.languages.Languages;
import org.multibit.hd.ui.languages.MessageKey;
import org.multibit.hd.ui.models.Models;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
/**
* <p>Utility to provide the following to application:</p>
* <ul>
* <li>Retrieving images from the Gravatar web service</li>
* </ul>
*
* @since 0.0.1
*
*/
public class Gravatars {
private static final Logger log = LoggerFactory.getLogger(Gravatars.class);
// Set the system defaults
private final static int SIZE = MultiBitUI.LARGE_ICON_SIZE;
private final static String RATING = Rating.GENERAL.getCode();
private final static String DEFAULT_IMAGE = DefaultImage.MYSTERY_MAN.getCode();
// Fixed entries
private final static String GRAVATAR_URL = "http://www.gravatar.com/avatar/";
private final static String PARAMETERS = "?s=" + SIZE + "&r=" + RATING + "&d=" + DEFAULT_IMAGE;
// Maintain a multi-threaded shared reference to a failure mode
private static AtomicReference<Optional<DateTime>> lastFailedDownload = new AtomicReference<>(Optional.<DateTime>absent());
// Keep an image thread pool
private static final ListeningExecutorService gravatarExecutorService = SafeExecutors.newFixedThreadPool(10, "gravatar");
// Maintain an image cache
private static LoadingCache<String, Optional<BufferedImage>> cache = CacheBuilder
.newBuilder()
.maximumSize(1000)
.build(new CacheLoader<String, Optional<BufferedImage>>() {
@Override
public Optional<BufferedImage> load(String cleanEmailAddress) throws Exception {
// Get the image synchronously (the overall cache call is wrapped in an executor)
return loadBufferedImage(cleanEmailAddress);
}
});
/**
* Utilities have private constructors
*/
private Gravatars() {
}
/**
* <p>Non-blocking call to retrieve a gravatar and provide notification on success or failure</p>
*
* @param emailAddress The email address
*
* @return A listenable future containing the corresponding image (default if the email address is unknown) or absent if an error occurs
*/
public static ListenableFuture<Optional<BufferedImage>> retrieveGravatar(final String emailAddress) {
Preconditions.checkNotNull(emailAddress, "'emailAddress' must be present");
final String cleanEmailAddress = emailAddress.toLowerCase().trim();
return gravatarExecutorService.submit(new Callable<Optional<BufferedImage>>() {
@Override
public Optional<BufferedImage> call() throws Exception {
return cache.get(cleanEmailAddress);
}
});
}
/**
* @param emailAddress The cleaned email address to use as an MD5 lookup
*
* @return The buffered image if present
*/
private static Optional<BufferedImage> loadBufferedImage(String emailAddress) {
log.debug("Loading image from external resource");
// Require a hex MD5 hash of email address (lowercase) no whitespace
final String emailHash = Hashing
.md5()
.hashString(emailAddress, Charsets.UTF_8)
.toString();
// Create the URL
final URL url;
try {
url = new URL(GRAVATAR_URL + emailHash + ".jpg" + PARAMETERS);
} catch (MalformedURLException e) {
// This should never happen
log.error("Gravatar URL malformed", e);
return Optional.absent();
}
try (InputStream stream = url.openStream()) {
return Optional.of(ImageIO.read(stream));
} catch (IOException e) {
// This may happen if no network is available
log.warn("Gravatar download failed" + e.getMessage());
// Avoid flooding the user with failure alerts
DateTime now = Dates.nowUtc();
if (lastFailedDownload.get().isPresent()) {
DateTime lastFailure = lastFailedDownload.get().get();
if (lastFailure.plusMinutes(1).isBefore(now)) {
// It's been a while since we had a failure so OK to notify the user again
ControllerEvents.fireAddAlertEvent(Models.newAlertModel(Languages.safeText(MessageKey.BITCOIN_NETWORK_CONFIGURATION_ERROR), RAGStatus.AMBER));
}
}
lastFailedDownload.set(Optional.of(now));
return Optional.absent();
}
}
}