aboutsummaryrefslogblamecommitdiffstats
path: root/e-util/e-photo-cache.c
blob: 6a03d10a51be1e10ac7374441e446df9e3796169 (plain) (tree)






















                                                                             
                                                              
  


                                                                        






                                    

                                  



                                                        



                                                                    





                                                                     


                                                                     
                                          

                                                      



                                    
                                   



                             


                               


                      






















                                    
                             




                                
                      



               
                         

  


                                                                              






                                         



























































                                                                           

                                                 
 
                                                

                                                          

                     

















































































































































                                                                           


                                                










                                                             
 


                                                      



                                                   


                                                           



                                                            
 
                                            
 


















                                                                              
         


                           

                                              
                                            























                                                            
                  
                              




                                              

                                         


                                                        




















                                                                 
                                                  

                                                          



                                                     

                                            
 
                             


                                         

                                                        


                                           
                     


           

                                            


                                         


                                                  

         

                                                        
















                                                                         



                                            





                              
                                                 












                                                          



                                                                     









                                                                     
                                                    















                                                                             

                                              









                                                                      
 
 







































                                                                             































































                                                                      



                                                          
 
                                 
 
                                                             
 



                                                                    
         

 



                                                          
 
                                                
 





                                               
 

                                               























                                                                      

















                                                                       


























                                                                       

                                                  

                                              


                                              










































                                                                          





                                             
                               






                                                   





                                                
                                                                    
                                                                               
                                               
                                                   

                                                         
                                                           







































                                                                         











































































































































                                                                              






                                                             

                                                               

















                                                                           


                               
 
                                         
 


                                                        
 
                                                
 

                                                        
 
                                       



                       







                                                                          

                                                                         












                                                                            


                                    



                                                          










                                                                            









                                                                            







                                                                      
 




































                                                                         
                                
                                      











                                                                         
                                                                            




























                                                                           



                                                                           




                    
/*
 * e-photo-cache.c
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) version 3.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with the program; if not, see <http://www.gnu.org/licenses/>
 *
 */

/**
 * SECTION: e-photo-cache
 * @include: e-util/e-util.h
 * @short_description: Search for photos by email address
 *
 * #EPhotoCache finds photos associated with an email address.
 *
 * A limited internal cache is employed to speed up frequently searched
 * email addresses.  The exact caching semantics are private and subject
 * to change.
 **/

#include "e-photo-cache.h"

#include <string.h>
#include <libebackend/libebackend.h>

#include <e-util/e-data-capture.h>

#define E_PHOTO_CACHE_GET_PRIVATE(obj) \
    (G_TYPE_INSTANCE_GET_PRIVATE \
    ((obj), E_TYPE_PHOTO_CACHE, EPhotoCachePrivate))

/* How long (in seconds) to hold out for a hit from the highest
 * priority photo source, after which we settle for what we have. */
#define ASYNC_TIMEOUT_SECONDS 3.0

/* How many email addresses we track at once, regardless of whether
 * the email address has a photo.  As new cache entries are added, we
 * discard the least recently accessed entries to keep the cache size
 * within the limit. */
#define MAX_CACHE_SIZE 20

#define ERROR_IS_CANCELLED(error) \
    (g_error_matches ((error), G_IO_ERROR, G_IO_ERROR_CANCELLED))

typedef struct _AsyncContext AsyncContext;
typedef struct _AsyncSubtask AsyncSubtask;
typedef struct _DataCaptureClosure DataCaptureClosure;
typedef struct _PhotoData PhotoData;

struct _EPhotoCachePrivate {
    EClientCache *client_cache;
    GMainContext *main_context;

    GHashTable *photo_ht;
    GQueue photo_ht_keys;
    GMutex photo_ht_lock;

    GHashTable *sources_ht;
    GMutex sources_ht_lock;
};

struct _AsyncContext {
    GMutex lock;
    GTimer *timer;
    GHashTable *subtasks;
    GQueue results;
    GInputStream *stream;
    GConverter *data_capture;

    GCancellable *cancellable;
    gulong cancelled_handler_id;
};

struct _AsyncSubtask {
    volatile gint ref_count;
    EPhotoSource *photo_source;
    GSimpleAsyncResult *simple;
    GCancellable *cancellable;
    GInputStream *stream;
    gint priority;
    GError *error;
};

struct _DataCaptureClosure {
    GWeakRef photo_cache;
    gchar *email_address;
};

struct _PhotoData {
    volatile gint ref_count;
    GMutex lock;
    GBytes *bytes;
};

enum {
    PROP_0,
    PROP_CLIENT_CACHE
};

/* Forward Declarations */
static void async_context_cancel_subtasks   (AsyncContext *async_context);

G_DEFINE_TYPE_WITH_CODE (
    EPhotoCache,
    e_photo_cache,
    G_TYPE_OBJECT,
    G_IMPLEMENT_INTERFACE (
        E_TYPE_EXTENSIBLE, NULL))

static AsyncSubtask *
async_subtask_new (EPhotoSource *photo_source,
                   GSimpleAsyncResult *simple)
{
    AsyncSubtask *async_subtask;

    async_subtask = g_slice_new0 (AsyncSubtask);
    async_subtask->ref_count = 1;
    async_subtask->photo_source = g_object_ref (photo_source);
    async_subtask->simple = g_object_ref (simple);
    async_subtask->cancellable = g_cancellable_new ();
    async_subtask->priority = G_PRIORITY_DEFAULT;

    return async_subtask;
}

static AsyncSubtask *
async_subtask_ref (AsyncSubtask *async_subtask)
{
    g_return_val_if_fail (async_subtask != NULL, NULL);
    g_return_val_if_fail (async_subtask->ref_count > 0, NULL);

    g_atomic_int_inc (&async_subtask->ref_count);

    return async_subtask;
}

static void
async_subtask_unref (AsyncSubtask *async_subtask)
{
    g_return_if_fail (async_subtask != NULL);
    g_return_if_fail (async_subtask->ref_count > 0);

    if (g_atomic_int_dec_and_test (&async_subtask->ref_count)) {

        /* Ignore cancellations. */
        if (ERROR_IS_CANCELLED (async_subtask->error))
            g_clear_error (&async_subtask->error);

        /* Leave a breadcrumb on the console
         * about unpropagated subtask errors. */
        if (async_subtask->error != NULL) {
            g_warning (
                "%s: Unpropagated error in %s subtask: %s",
                __FILE__,
                G_OBJECT_TYPE_NAME (
                async_subtask->photo_source),
                async_subtask->error->message);
            g_error_free (async_subtask->error);
        }

        g_clear_object (&async_subtask->photo_source);
        g_clear_object (&async_subtask->simple);
        g_clear_object (&async_subtask->cancellable);
        g_clear_object (&async_subtask->stream);

        g_slice_free (AsyncSubtask, async_subtask);
    }
}

static gboolean
async_subtask_cancel_idle_cb (gpointer user_data)
{
    AsyncSubtask *async_subtask = user_data;

    g_cancellable_cancel (async_subtask->cancellable);

    return FALSE;
}

static gint
async_subtask_compare (gconstpointer a,
                       gconstpointer b)
{
    const AsyncSubtask *subtask_a = a;
    const AsyncSubtask *subtask_b = b;

    /* Without error is always less than with error. */

    if (subtask_a->error != NULL && subtask_b->error != NULL)
        return 0;

    if (subtask_a->error == NULL && subtask_b->error != NULL)
        return -1;

    if (subtask_a->error != NULL && subtask_b->error == NULL)
        return 1;

    if (subtask_a->priority == subtask_b->priority)
        return 0;

    return (subtask_a->priority < subtask_b->priority) ? -1 : 1;
}

static void
async_subtask_complete (AsyncSubtask *async_subtask)
{
    GSimpleAsyncResult *simple;
    AsyncContext *async_context;
    gboolean cancel_subtasks = FALSE;
    gdouble seconds_elapsed;

    simple = async_subtask->simple;
    async_context = g_simple_async_result_get_op_res_gpointer (simple);

    g_mutex_lock (&async_context->lock);

    seconds_elapsed = g_timer_elapsed (async_context->timer, NULL);

    /* Discard successfully completed subtasks with no match found.
     * Keep failed subtasks around so we have a GError to propagate
     * if we need one, but those go on the end of the queue. */

    if (async_subtask->stream != NULL) {
        g_queue_insert_sorted (
            &async_context->results,
            async_subtask_ref (async_subtask),
            (GCompareDataFunc) async_subtask_compare,
            NULL);

        /* If enough seconds have elapsed, just take the highest
         * priority input stream we have.  Cancel the unfinished
         * subtasks and let them complete with an error. */
        if (seconds_elapsed > ASYNC_TIMEOUT_SECONDS)
            cancel_subtasks = TRUE;

    } else if (async_subtask->error != NULL) {
        g_queue_push_tail (
            &async_context->results,
            async_subtask_ref (async_subtask));
    }

    g_hash_table_remove (async_context->subtasks, async_subtask);

    if (g_hash_table_size (async_context->subtasks) > 0) {
        /* Let the remaining subtasks finish. */
        goto exit;
    }

    /* The queue should be ordered now such that subtasks
     * with input streams are before subtasks with errors.
     * So just evaluate the first subtask on the queue. */

    async_subtask = g_queue_pop_head (&async_context->results);

    if (async_subtask != NULL) {
        if (async_subtask->stream != NULL) {
            async_context->stream =
                g_converter_input_stream_new (
                    async_subtask->stream,
                    async_context->data_capture);
        }

        if (async_subtask->error != NULL) {
            g_simple_async_result_take_error (
                simple, async_subtask->error);
            async_subtask->error = NULL;
        }

        async_subtask_unref (async_subtask);
    }

    g_simple_async_result_complete_in_idle (simple);

exit:
    g_mutex_unlock (&async_context->lock);

    if (cancel_subtasks) {
        /* Call this after the mutex is unlocked. */
        async_context_cancel_subtasks (async_context);
    }
}

static void
async_context_cancelled_cb (GCancellable *cancellable,
                            AsyncContext *async_context)
{
    async_context_cancel_subtasks (async_context);
}

static AsyncContext *
async_context_new (EDataCapture *data_capture,
                   GCancellable *cancellable)
{
    AsyncContext *async_context;

    async_context = g_slice_new0 (AsyncContext);
    g_mutex_init (&async_context->lock);
    async_context->timer = g_timer_new ();

    async_context->subtasks = g_hash_table_new_full (
        (GHashFunc) g_direct_hash,
        (GEqualFunc) g_direct_equal,
        (GDestroyNotify) async_subtask_unref,
        (GDestroyNotify) NULL);

    async_context->data_capture = g_object_ref (data_capture);

    if (G_IS_CANCELLABLE (cancellable)) {
        gulong handler_id;

        async_context->cancellable = g_object_ref (cancellable);

        handler_id = g_cancellable_connect (
            async_context->cancellable,
            G_CALLBACK (async_context_cancelled_cb),
            async_context,
            (GDestroyNotify) NULL);
        async_context->cancelled_handler_id = handler_id;
    }

    return async_context;
}

static void
async_context_free (AsyncContext *async_context)
{
    /* Do this first so the callback won't fire
     * while we're dismantling the AsyncContext. */
    if (async_context->cancelled_handler_id > 0)
        g_cancellable_disconnect (
            async_context->cancellable,
            async_context->cancelled_handler_id);

    g_mutex_clear (&async_context->lock);
    g_timer_destroy (async_context->timer);

    g_hash_table_destroy (async_context->subtasks);

    g_clear_object (&async_context->stream);
    g_clear_object (&async_context->data_capture);
    g_clear_object (&async_context->cancellable);

    g_slice_free (AsyncContext, async_context);
}

static void
async_context_cancel_subtasks (AsyncContext *async_context)
{
    GMainContext *main_context;
    GList *list, *link;

    main_context = g_main_context_ref_thread_default ();

    g_mutex_lock (&async_context->lock);

    list = g_hash_table_get_keys (async_context->subtasks);

    /* XXX Cancel subtasks from idle callbacks to make sure we don't
     *     finalize the GSimpleAsyncResult during a "cancelled" signal
     *     emission from the main task's GCancellable.  That will make
     *     g_cancellable_disconnect() in async_context_free() deadlock. */
    for (link = list; link != NULL; link = g_list_next (link)) {
        AsyncSubtask *async_subtask = link->data;
        GSource *idle_source;

        idle_source = g_idle_source_new ();
        g_source_set_priority (idle_source, G_PRIORITY_HIGH_IDLE);
        g_source_set_callback (
            idle_source,
            async_subtask_cancel_idle_cb,
            async_subtask_ref (async_subtask),
            (GDestroyNotify) async_subtask_unref);
        g_source_attach (idle_source, main_context);
        g_source_unref (idle_source);
    }

    g_list_free (list);

    g_mutex_unlock (&async_context->lock);

    g_main_context_unref (main_context);
}

static DataCaptureClosure *
data_capture_closure_new (EPhotoCache *photo_cache,
                          const gchar *email_address)
{
    DataCaptureClosure *closure;

    closure = g_slice_new0 (DataCaptureClosure);
    g_weak_ref_set (&closure->photo_cache, photo_cache);
    closure->email_address = g_strdup (email_address);

    return closure;
}

static void
data_capture_closure_free (DataCaptureClosure *closure)
{
    g_weak_ref_set (&closure->photo_cache, NULL);
    g_free (closure->email_address);

    g_slice_free (DataCaptureClosure, closure);
}

static PhotoData *
photo_data_new (GBytes *bytes)
{
    PhotoData *photo_data;

    photo_data = g_slice_new0 (PhotoData);
    photo_data->ref_count = 1;
    g_mutex_init (&photo_data->lock);

    if (bytes != NULL)
        photo_data->bytes = g_bytes_ref (bytes);

    return photo_data;
}

static PhotoData *
photo_data_ref (PhotoData *photo_data)
{
    g_return_val_if_fail (photo_data != NULL, NULL);
    g_return_val_if_fail (photo_data->ref_count > 0, NULL);

    g_atomic_int_inc (&photo_data->ref_count);

    return photo_data;
}

static void
photo_data_unref (PhotoData *photo_data)
{
    g_return_if_fail (photo_data != NULL);
    g_return_if_fail (photo_data->ref_count > 0);

    if (g_atomic_int_dec_and_test (&photo_data->ref_count)) {
        g_mutex_clear (&photo_data->lock);
        if (photo_data->bytes != NULL)
            g_bytes_unref (photo_data->bytes);
        g_slice_free (PhotoData, photo_data);
    }
}

static GBytes *
photo_data_ref_bytes (PhotoData *photo_data)
{
    GBytes *bytes = NULL;

    g_mutex_lock (&photo_data->lock);

    if (photo_data->bytes != NULL)
        bytes = g_bytes_ref (photo_data->bytes);

    g_mutex_unlock (&photo_data->lock);

    return bytes;
}

static void
photo_data_set_bytes (PhotoData *photo_data,
                      GBytes *bytes)
{
    g_mutex_lock (&photo_data->lock);

    if (photo_data->bytes != NULL) {
        g_bytes_unref (photo_data->bytes);
        photo_data->bytes = NULL;
    }

    if (bytes != NULL)
        photo_data->bytes = g_bytes_ref (bytes);

    g_mutex_unlock (&photo_data->lock);
}

static gchar *
photo_ht_normalize_key (const gchar *email_address)
{
    gchar *lowercase_email_address;
    gchar *collation_key;

    lowercase_email_address = g_utf8_strdown (email_address, -1);
    collation_key = g_utf8_collate_key (lowercase_email_address, -1);
    g_free (lowercase_email_address);

    return collation_key;
}

static void
photo_ht_insert (EPhotoCache *photo_cache,
                 const gchar *email_address,
                 GBytes *bytes)
{
    GHashTable *photo_ht;
    GQueue *photo_ht_keys;
    PhotoData *photo_data;
    gchar *key;

    g_return_if_fail (email_address != NULL);

    photo_ht = photo_cache->priv->photo_ht;
    photo_ht_keys = &photo_cache->priv->photo_ht_keys;

    key = photo_ht_normalize_key (email_address);

    g_mutex_lock (&photo_cache->priv->photo_ht_lock);

    photo_data = g_hash_table_lookup (photo_ht, key);

    if (photo_data != NULL) {
        GList *link;

        /* Replace the old photo data if we have new photo
         * data, otherwise leave the old photo data alone. */
        if (bytes != NULL)
            photo_data_set_bytes (photo_data, bytes);

        /* Move the key to the head of the MRU queue. */
        link = g_queue_find_custom (
            photo_ht_keys, key,
            (GCompareFunc) strcmp);
        if (link != NULL) {
            g_queue_unlink (photo_ht_keys, link);
            g_queue_push_head_link (photo_ht_keys, link);
        }
    } else {
        photo_data = photo_data_new (bytes);

        g_hash_table_insert (
            photo_ht, g_strdup (key),
            photo_data_ref (photo_data));

        /* Push the key to the head of the MRU queue. */
        g_queue_push_head (photo_ht_keys, g_strdup (key));

        /* Trim the cache if necessary. */
        while (g_queue_get_length (photo_ht_keys) > MAX_CACHE_SIZE) {
            gchar *oldest_key;

            oldest_key = g_queue_pop_tail (photo_ht_keys);
            g_hash_table_remove (photo_ht, oldest_key);
            g_free (oldest_key);
        }

        photo_data_unref (photo_data);
    }

    /* Hash table and queue sizes should be equal at all times. */
    g_warn_if_fail (
        g_hash_table_size (photo_ht) ==
        g_queue_get_length (photo_ht_keys));

    g_mutex_unlock (&photo_cache->priv->photo_ht_lock);

    g_free (key);
}

static gboolean
photo_ht_lookup (EPhotoCache *photo_cache,
                 const gchar *email_address,
                 GInputStream **out_stream)
{
    GHashTable *photo_ht;
    PhotoData *photo_data;
    gboolean found = FALSE;
    gchar *key;

    g_return_val_if_fail (email_address != NULL, FALSE);
    g_return_val_if_fail (out_stream != NULL, FALSE);

    photo_ht = photo_cache->priv->photo_ht;

    key = photo_ht_normalize_key (email_address);

    g_mutex_lock (&photo_cache->priv->photo_ht_lock);

    photo_data = g_hash_table_lookup (photo_ht, key);

    if (photo_data != NULL) {
        GBytes *bytes;

        bytes = photo_data_ref_bytes (photo_data);
        if (bytes != NULL) {
            *out_stream =
                g_memory_input_stream_new_from_bytes (bytes);
            g_bytes_unref (bytes);
        } else {
            *out_stream = NULL;
        }
        found = TRUE;
    }

    g_mutex_unlock (&photo_cache->priv->photo_ht_lock);

    g_free (key);

    return found;
}

static gboolean
photo_ht_remove (EPhotoCache *photo_cache,
                 const gchar *email_address)
{
    GHashTable *photo_ht;
    GQueue *photo_ht_keys;
    gchar *key;
    gboolean removed = FALSE;

    g_return_val_if_fail (email_address != NULL, FALSE);

    photo_ht = photo_cache->priv->photo_ht;
    photo_ht_keys = &photo_cache->priv->photo_ht_keys;

    key = photo_ht_normalize_key (email_address);

    g_mutex_lock (&photo_cache->priv->photo_ht_lock);

    if (g_hash_table_remove (photo_ht, key)) {
        GList *link;

        link = g_queue_find_custom (
            photo_ht_keys, key,
            (GCompareFunc) strcmp);
        if (link != NULL) {
            g_free (link->data);
            g_queue_delete_link (photo_ht_keys, link);
            removed = TRUE;
        }
    }

    /* Hash table and queue sizes should be equal at all times. */
    g_warn_if_fail (
        g_hash_table_size (photo_ht) ==
        g_queue_get_length (photo_ht_keys));

    g_mutex_unlock (&photo_cache->priv->photo_ht_lock);

    g_free (key);

    return removed;
}

static void
photo_ht_remove_all (EPhotoCache *photo_cache)
{
    GHashTable *photo_ht;
    GQueue *photo_ht_keys;

    photo_ht = photo_cache->priv->photo_ht;
    photo_ht_keys = &photo_cache->priv->photo_ht_keys;

    g_mutex_lock (&photo_cache->priv->photo_ht_lock);

    g_hash_table_remove_all (photo_ht);

    while (!g_queue_is_empty (photo_ht_keys))
        g_free (g_queue_pop_head (photo_ht_keys));

    g_mutex_unlock (&photo_cache->priv->photo_ht_lock);
}

static void
photo_cache_data_captured_cb (EDataCapture *data_capture,
                              GBytes *bytes,
                              DataCaptureClosure *closure)
{
    EPhotoCache *photo_cache;

    photo_cache = g_weak_ref_get (&closure->photo_cache);

    if (photo_cache != NULL) {
        e_photo_cache_add_photo (
            photo_cache, closure->email_address, bytes);
        g_object_unref (photo_cache);
    }
}

static void
photo_cache_async_subtask_done_cb (GObject *source_object,
                                   GAsyncResult *result,
                                   gpointer user_data)
{
    AsyncSubtask *async_subtask = user_data;

    e_photo_source_get_photo_finish (
        E_PHOTO_SOURCE (source_object),
        result,
        &async_subtask->stream,
        &async_subtask->priority,
        &async_subtask->error);

    async_subtask_complete (async_subtask);
    async_subtask_unref (async_subtask);
}

static void
photo_cache_set_client_cache (EPhotoCache *photo_cache,
                              EClientCache *client_cache)
{
    g_return_if_fail (E_IS_CLIENT_CACHE (client_cache));
    g_return_if_fail (photo_cache->priv->client_cache == NULL);

    photo_cache->priv->client_cache = g_object_ref (client_cache);
}

static void
photo_cache_set_property (GObject *object,
                          guint property_id,
                          const GValue *value,
                          GParamSpec *pspec)
{
    switch (property_id) {
        case PROP_CLIENT_CACHE:
            photo_cache_set_client_cache (
                E_PHOTO_CACHE (object),
                g_value_get_object (value));
            return;
    }

    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
photo_cache_get_property (GObject *object,
                          guint property_id,
                          GValue *value,
                          GParamSpec *pspec)
{
    switch (property_id) {
        case PROP_CLIENT_CACHE:
            g_value_take_object (
                value,
                e_photo_cache_ref_client_cache (
                E_PHOTO_CACHE (object)));
            return;
    }

    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}

static void
photo_cache_dispose (GObject *object)
{
    EPhotoCachePrivate *priv;

    priv = E_PHOTO_CACHE_GET_PRIVATE (object);

    g_clear_object (&priv->client_cache);

    photo_ht_remove_all (E_PHOTO_CACHE (object));

    /* Chain up to parent's dispose() method. */
    G_OBJECT_CLASS (e_photo_cache_parent_class)->dispose (object);
}

static void
photo_cache_finalize (GObject *object)
{
    EPhotoCachePrivate *priv;

    priv = E_PHOTO_CACHE_GET_PRIVATE (object);

    g_main_context_unref (priv->main_context);

    g_hash_table_destroy (priv->photo_ht);

    g_mutex_lock (&priv->photo_ht_lock);
    g_mutex_lock (&priv->sources_ht_lock);

    /* Chain up to parent's finalize() method. */
    G_OBJECT_CLASS (e_photo_cache_parent_class)->finalize (object);
}

static void
photo_cache_constructed (GObject *object)
{
    /* Chain up to parent's constructed() method. */
    G_OBJECT_CLASS (e_photo_cache_parent_class)->constructed (object);

    e_extensible_load_extensions (E_EXTENSIBLE (object));
}

static void
e_photo_cache_class_init (EPhotoCacheClass *class)
{
    GObjectClass *object_class;

    g_type_class_add_private (class, sizeof (EPhotoCachePrivate));

    object_class = G_OBJECT_CLASS (class);
    object_class->set_property = photo_cache_set_property;
    object_class->get_property = photo_cache_get_property;
    object_class->dispose = photo_cache_dispose;
    object_class->finalize = photo_cache_finalize;
    object_class->constructed = photo_cache_constructed;

    /**
     * EPhotoCache:client-cache:
     *
     * Cache of shared #EClient instances.
     **/
    g_object_class_install_property (
        object_class,
        PROP_CLIENT_CACHE,
        g_param_spec_object (
            "client-cache",
            "Client Cache",
            "Cache of shared EClient instances",
            E_TYPE_CLIENT_CACHE,
            G_PARAM_READWRITE |
            G_PARAM_CONSTRUCT_ONLY |
            G_PARAM_STATIC_STRINGS));
}

static void
e_photo_cache_init (EPhotoCache *photo_cache)
{
    GHashTable *photo_ht;
    GHashTable *sources_ht;

    photo_ht = g_hash_table_new_full (
        (GHashFunc) g_str_hash,
        (GEqualFunc) g_str_equal,
        (GDestroyNotify) g_free,
        (GDestroyNotify) photo_data_unref);

    sources_ht = g_hash_table_new_full (
        (GHashFunc) g_direct_hash,
        (GEqualFunc) g_direct_equal,
        (GDestroyNotify) g_object_unref,
        (GDestroyNotify) NULL);

    photo_cache->priv = E_PHOTO_CACHE_GET_PRIVATE (photo_cache);
    photo_cache->priv->main_context = g_main_context_ref_thread_default ();
    photo_cache->priv->photo_ht = photo_ht;
    photo_cache->priv->sources_ht = sources_ht;

    g_mutex_init (&photo_cache->priv->photo_ht_lock);
    g_mutex_init (&photo_cache->priv->sources_ht_lock);
}

/**
 * e_photo_cache_new:
 * @client_cache: an #EClientCache
 *
 * Creates a new #EPhotoCache instance.
 *
 * Returns: an #EPhotoCache
 **/
EPhotoCache *
e_photo_cache_new (EClientCache *client_cache)
{
    g_return_val_if_fail (E_IS_CLIENT_CACHE (client_cache), NULL);

    return g_object_new (
        E_TYPE_PHOTO_CACHE,
        "client-cache", client_cache, NULL);
}

/**
 * e_photo_cache_ref_client_cache:
 * @photo_cache: an #EPhotoCache
 *
 * Returns the #EClientCache passed to e_photo_cache_new().
 *
 * The returned #EClientCache is referenced for thread-safety and must be
 * unreferenced with g_object_unref() when finished with it.
 *
 * Returns: an #EClientCache
 **/
EClientCache *
e_photo_cache_ref_client_cache (EPhotoCache *photo_cache)
{
    g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), NULL);

    return g_object_ref (photo_cache->priv->client_cache);
}

/**
 * e_photo_cache_add_photo_source:
 * @photo_cache: an #EPhotoCache
 * @photo_source: an #EPhotoSource
 *
 * Adds @photo_source as a potential source of photos.
 **/
void
e_photo_cache_add_photo_source (EPhotoCache *photo_cache,
                                EPhotoSource *photo_source)
{
    GHashTable *sources_ht;

    g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache));
    g_return_if_fail (E_IS_PHOTO_SOURCE (photo_source));

    sources_ht = photo_cache->priv->sources_ht;

    g_mutex_lock (&photo_cache->priv->sources_ht_lock);

    g_hash_table_add (sources_ht, g_object_ref (photo_source));

    g_mutex_unlock (&photo_cache->priv->sources_ht_lock);
}

/**
 * e_photo_cache_list_photo_sources:
 * @photo_cache: an #EPhotoCache
 *
 * Returns a list of photo sources for @photo_cache.
 *
 * The sources returned in the list are referenced for thread-safety.
 * They must each be unreferenced with g_object_unref() when finished
 * with them.  Free the returned list itself with g_list_free().
 *
 * An easy way to free the list property in one step is as follows:
 *
 * |[
 *   g_list_free_full (list, g_object_unref);
 * ]|
 *
 * Returns: a sorted list of photo sources
 **/
GList *
e_photo_cache_list_photo_sources (EPhotoCache *photo_cache)
{
    GHashTable *sources_ht;
    GList *list;

    g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), NULL);

    sources_ht = photo_cache->priv->sources_ht;

    g_mutex_lock (&photo_cache->priv->sources_ht_lock);

    list = g_hash_table_get_keys (sources_ht);
    g_list_foreach (list, (GFunc) g_object_ref, NULL);

    g_mutex_unlock (&photo_cache->priv->sources_ht_lock);

    return list;
}

/**
 * e_photo_cache_remove_photo_source:
 * @photo_cache: an #EPhotoCache
 * @photo_source: an #EPhotoSource
 *
 * Removes @photo_source as a potential source of photos.
 *
 * Returns: %TRUE if @photo_source was found and removed, %FALSE if not
 **/
gboolean
e_photo_cache_remove_photo_source (EPhotoCache *photo_cache,
                                   EPhotoSource *photo_source)
{
    GHashTable *sources_ht;
    gboolean removed;

    g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE);
    g_return_val_if_fail (E_IS_PHOTO_SOURCE (photo_source), FALSE);

    sources_ht = photo_cache->priv->sources_ht;

    g_mutex_lock (&photo_cache->priv->sources_ht_lock);

    removed = g_hash_table_remove (sources_ht, photo_source);

    g_mutex_unlock (&photo_cache->priv->sources_ht_lock);

    return removed;
}

/**
 * e_photo_cache_add_photo:
 * @photo_cache: an #EPhotoCache
 * @email_address: an email address
 * @bytes: a #GBytes containing photo data, or %NULL
 *
 * Adds a cache entry for @email_address, such that subsequent photo requests
 * for @email_address will yield a #GMemoryInputStream loaded with @bytes
 * without consulting available photo sources.
 *
 * The @bytes argument can also be %NULL to indicate no photo is available for
 * @email_address.  Subsequent photo requests for @email_address will yield no
 * input stream.
 *
 * The entry may be removed without notice however, subject to @photo_cache's
 * internal caching policy.
 **/
void
e_photo_cache_add_photo (EPhotoCache *photo_cache,
                         const gchar *email_address,
                         GBytes *bytes)
{
    g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache));
    g_return_if_fail (email_address != NULL);

    photo_ht_insert (photo_cache, email_address, bytes);
}

/**
 * e_photo_cache_remove_photo:
 * @photo_cache: an #EPhotoCache
 * @email_address: an email address
 *
 * Removes the cache entry for @email_address, if such an entry exists.
 *
 * Returns: %TRUE if a cache entry was found and removed
 **/
gboolean
e_photo_cache_remove_photo (EPhotoCache *photo_cache,
                            const gchar *email_address)
{
    g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE);
    g_return_val_if_fail (email_address != NULL, FALSE);

    return photo_ht_remove (photo_cache, email_address);
}

/**
 * e_photo_cache_get_photo_sync:
 * @photo_cache: an #EPhotoCache
 * @email_address: an email address
 * @cancellable: optional #GCancellable object, or %NULL
 * @out_stream: return location for a #GInputStream, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Searches available photo sources for a photo associated with
 * @email_address.
 *
 * If a match is found, a #GInputStream from which to read image data is
 * returned through the @out_stream return location.  If no match is found,
 * the @out_stream return location is set to %NULL.
 *
 * The return value indicates whether the search completed successfully,
 * not whether a match was found.  If an error occurs, the function will
 * set @error and return %FALSE.
 *
 * Returns: whether the search completed successfully
 **/
gboolean
e_photo_cache_get_photo_sync (EPhotoCache *photo_cache,
                              const gchar *email_address,
                              GCancellable *cancellable,
                              GInputStream **out_stream,
                              GError **error)
{
    EAsyncClosure *closure;
    GAsyncResult *result;
    gboolean success;

    closure = e_async_closure_new ();

    e_photo_cache_get_photo (
        photo_cache, email_address, cancellable,
        e_async_closure_callback, closure);

    result = e_async_closure_wait (closure);

    success = e_photo_cache_get_photo_finish (
        photo_cache, result, out_stream, error);

    e_async_closure_free (closure);

    return success;
}

/**
 * e_photo_cache_get_photo:
 * @photo_cache: an #EPhotoCache
 * @email_address: an email address
 * @cancellable: optional #GCancellable object, or %NULL
 * @callback: a #GAsyncReadyCallback to call when the request is satisfied
 * @user_data: data to pass to the callback function
 *
 * Asynchronously searches available photo sources for a photo associated
 * with @email_address.
 *
 * When the operation is finished, @callback will be called.  You can then
 * call e_photo_cache_get_photo_finish() to get the result of the operation.
 **/
void
e_photo_cache_get_photo (EPhotoCache *photo_cache,
                         const gchar *email_address,
                         GCancellable *cancellable,
                         GAsyncReadyCallback callback,
                         gpointer user_data)
{
    GSimpleAsyncResult *simple;
    AsyncContext *async_context;
    EDataCapture *data_capture;
    GInputStream *stream = NULL;
    GList *list, *link;

    g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache));
    g_return_if_fail (email_address != NULL);

    /* This will be used to eavesdrop on the resulting input stream
     * for the purpose of adding the photo data to the photo cache. */
    data_capture = e_data_capture_new (photo_cache->priv->main_context);

    g_signal_connect_data (
        data_capture, "finished",
        G_CALLBACK (photo_cache_data_captured_cb),
        data_capture_closure_new (photo_cache, email_address),
        (GClosureNotify) data_capture_closure_free, 0);

    async_context = async_context_new (data_capture, cancellable);

    simple = g_simple_async_result_new (
        G_OBJECT (photo_cache), callback,
        user_data, e_photo_cache_get_photo);

    g_simple_async_result_set_check_cancellable (simple, cancellable);

    g_simple_async_result_set_op_res_gpointer (
        simple, async_context, (GDestroyNotify) async_context_free);

    /* Check if we have this email address already cached. */
    if (photo_ht_lookup (photo_cache, email_address, &stream)) {
        async_context->stream = stream;  /* takes ownership */
        g_simple_async_result_complete_in_idle (simple);
        goto exit;
    }

    list = e_photo_cache_list_photo_sources (photo_cache);

    if (list == NULL) {
        g_simple_async_result_complete_in_idle (simple);
        goto exit;
    }

    g_mutex_lock (&async_context->lock);

    /* Dispatch a subtask for each photo source. */
    for (link = list; link != NULL; link = g_list_next (link)) {
        EPhotoSource *photo_source;
        AsyncSubtask *async_subtask;

        photo_source = E_PHOTO_SOURCE (link->data);
        async_subtask = async_subtask_new (photo_source, simple);

        g_hash_table_add (
            async_context->subtasks,
            async_subtask_ref (async_subtask));

        e_photo_source_get_photo (
            photo_source, email_address,
            async_subtask->cancellable,
            photo_cache_async_subtask_done_cb,
            async_subtask_ref (async_subtask));

        async_subtask_unref (async_subtask);
    }

    g_mutex_unlock (&async_context->lock);

    g_list_free_full (list, (GDestroyNotify) g_object_unref);

    /* Check if we were cancelled while dispatching subtasks. */
    if (g_cancellable_is_cancelled (cancellable))
        async_context_cancel_subtasks (async_context);

exit:
    g_object_unref (simple);
    g_object_unref (data_capture);
}

/**
 * e_photo_cache_get_photo_finish:
 * @photo_cache: an #EPhotoCache
 * @result: a #GAsyncResult
 * @out_stream: return location for a #GInputStream, or %NULL
 * @error: return location for a #GError, or %NULL
 *
 * Finishes the operation started with e_photo_cache_get_photo().
 *
 * If a match was found, a #GInputStream from which to read image data is
 * returned through the @out_stream return location.  If no match was found,
 * the @out_stream return location is set to %NULL.
 *
 * The return value indicates whether the search completed successfully,
 * not whether a match was found.  If an error occurred, the function will
 * set @error and return %FALSE.
 *
 * Returns: whether the search completed successfully
 **/
gboolean
e_photo_cache_get_photo_finish (EPhotoCache *photo_cache,
                                GAsyncResult *result,
                                GInputStream **out_stream,
                                GError **error)
{
    GSimpleAsyncResult *simple;
    AsyncContext *async_context;

    g_return_val_if_fail (
        g_simple_async_result_is_valid (
        result, G_OBJECT (photo_cache),
        e_photo_cache_get_photo), FALSE);

    simple = G_SIMPLE_ASYNC_RESULT (result);
    async_context = g_simple_async_result_get_op_res_gpointer (simple);

    if (g_simple_async_result_propagate_error (simple, error))
        return FALSE;

    if (out_stream != NULL) {
        if (async_context->stream != NULL)
            *out_stream = g_object_ref (async_context->stream);
        else
            *out_stream = NULL;
    }

    return TRUE;
}