1 /*
2 * e-photo-cache.c
3 *
4 * This program is free software; you can redistribute it and/or modify it
5 * under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation.
7 *
8 * This program is distributed in the hope that it will be useful, but
9 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
11 * for more details.
12 *
13 * You should have received a copy of the GNU Lesser General Public License
14 * along with this program; if not, see <http://www.gnu.org/licenses/>.
15 *
16 */
17
18 /**
19 * SECTION: e-photo-cache
20 * @include: e-util/e-util.h
21 * @short_description: Search for photos by email address
22 *
23 * #EPhotoCache finds photos associated with an email address.
24 *
25 * A limited internal cache is employed to speed up frequently searched
26 * email addresses. The exact caching semantics are private and subject
27 * to change.
28 **/
29
30 #include "e-photo-cache.h"
31
32 #include <string.h>
33 #include <libebackend/libebackend.h>
34
35 #include <e-util/e-data-capture.h>
36
37 #define E_PHOTO_CACHE_GET_PRIVATE(obj) \
38 (G_TYPE_INSTANCE_GET_PRIVATE \
39 ((obj), E_TYPE_PHOTO_CACHE, EPhotoCachePrivate))
40
41 /* How long (in seconds) to hold out for a hit from the highest
42 * priority photo source, after which we settle for what we have. */
43 #define ASYNC_TIMEOUT_SECONDS 3.0
44
45 /* How many email addresses we track at once, regardless of whether
46 * the email address has a photo. As new cache entries are added, we
47 * discard the least recently accessed entries to keep the cache size
48 * within the limit. */
49 #define MAX_CACHE_SIZE 20
50
51 #define ERROR_IS_CANCELLED(error) \
52 (g_error_matches ((error), G_IO_ERROR, G_IO_ERROR_CANCELLED))
53
54 typedef struct _AsyncContext AsyncContext;
55 typedef struct _AsyncSubtask AsyncSubtask;
56 typedef struct _DataCaptureClosure DataCaptureClosure;
57 typedef struct _PhotoData PhotoData;
58
59 struct _EPhotoCachePrivate {
60 EClientCache *client_cache;
61 GMainContext *main_context;
62
63 GHashTable *photo_ht;
64 GQueue photo_ht_keys;
65 GMutex photo_ht_lock;
66
67 GHashTable *sources_ht;
68 GMutex sources_ht_lock;
69 };
70
71 struct _AsyncContext {
72 GMutex lock;
73 GTimer *timer;
74 GHashTable *subtasks;
75 GQueue results;
76 GInputStream *stream;
77 GConverter *data_capture;
78
79 GCancellable *cancellable;
80 gulong cancelled_handler_id;
81 };
82
83 struct _AsyncSubtask {
84 volatile gint ref_count;
85 EPhotoSource *photo_source;
86 GSimpleAsyncResult *simple;
87 GCancellable *cancellable;
88 GInputStream *stream;
89 gint priority;
90 GError *error;
91 };
92
93 struct _DataCaptureClosure {
94 GWeakRef photo_cache;
95 gchar *email_address;
96 };
97
98 struct _PhotoData {
99 volatile gint ref_count;
100 GMutex lock;
101 GBytes *bytes;
102 };
103
104 enum {
105 PROP_0,
106 PROP_CLIENT_CACHE
107 };
108
109 /* Forward Declarations */
110 static void async_context_cancel_subtasks (AsyncContext *async_context);
111
G_DEFINE_TYPE_WITH_CODE(EPhotoCache,e_photo_cache,G_TYPE_OBJECT,G_IMPLEMENT_INTERFACE (E_TYPE_EXTENSIBLE,NULL))112 G_DEFINE_TYPE_WITH_CODE (
113 EPhotoCache,
114 e_photo_cache,
115 G_TYPE_OBJECT,
116 G_IMPLEMENT_INTERFACE (
117 E_TYPE_EXTENSIBLE, NULL))
118
119 static AsyncSubtask *
120 async_subtask_new (EPhotoSource *photo_source,
121 GSimpleAsyncResult *simple)
122 {
123 AsyncSubtask *async_subtask;
124
125 async_subtask = g_slice_new0 (AsyncSubtask);
126 async_subtask->ref_count = 1;
127 async_subtask->photo_source = g_object_ref (photo_source);
128 async_subtask->simple = g_object_ref (simple);
129 async_subtask->cancellable = g_cancellable_new ();
130 async_subtask->priority = G_PRIORITY_DEFAULT;
131
132 return async_subtask;
133 }
134
135 static AsyncSubtask *
async_subtask_ref(AsyncSubtask * async_subtask)136 async_subtask_ref (AsyncSubtask *async_subtask)
137 {
138 g_return_val_if_fail (async_subtask != NULL, NULL);
139 g_return_val_if_fail (async_subtask->ref_count > 0, NULL);
140
141 g_atomic_int_inc (&async_subtask->ref_count);
142
143 return async_subtask;
144 }
145
146 static void
async_subtask_unref(AsyncSubtask * async_subtask)147 async_subtask_unref (AsyncSubtask *async_subtask)
148 {
149 g_return_if_fail (async_subtask != NULL);
150 g_return_if_fail (async_subtask->ref_count > 0);
151
152 if (g_atomic_int_dec_and_test (&async_subtask->ref_count)) {
153
154 /* Ignore cancellations. */
155 if (ERROR_IS_CANCELLED (async_subtask->error))
156 g_clear_error (&async_subtask->error);
157
158 /* Leave a breadcrumb on the console
159 * about unpropagated subtask errors. */
160 if (async_subtask->error != NULL) {
161 g_warning (
162 "%s: Unpropagated error in %s subtask: %s",
163 __FILE__,
164 G_OBJECT_TYPE_NAME (
165 async_subtask->photo_source),
166 async_subtask->error->message);
167 g_error_free (async_subtask->error);
168 }
169
170 g_clear_object (&async_subtask->photo_source);
171 g_clear_object (&async_subtask->simple);
172 g_clear_object (&async_subtask->cancellable);
173 g_clear_object (&async_subtask->stream);
174
175 g_slice_free (AsyncSubtask, async_subtask);
176 }
177 }
178
179 static gboolean
async_subtask_cancel_idle_cb(gpointer user_data)180 async_subtask_cancel_idle_cb (gpointer user_data)
181 {
182 AsyncSubtask *async_subtask = user_data;
183
184 g_cancellable_cancel (async_subtask->cancellable);
185
186 return FALSE;
187 }
188
189 static gint
async_subtask_compare(gconstpointer a,gconstpointer b)190 async_subtask_compare (gconstpointer a,
191 gconstpointer b)
192 {
193 const AsyncSubtask *subtask_a = a;
194 const AsyncSubtask *subtask_b = b;
195
196 /* Without error is always less than with error. */
197
198 if (subtask_a->error != NULL && subtask_b->error != NULL)
199 return 0;
200
201 if (subtask_a->error == NULL && subtask_b->error != NULL)
202 return -1;
203
204 if (subtask_a->error != NULL && subtask_b->error == NULL)
205 return 1;
206
207 if (subtask_a->priority == subtask_b->priority)
208 return 0;
209
210 return (subtask_a->priority < subtask_b->priority) ? -1 : 1;
211 }
212
213 static void
async_subtask_complete(AsyncSubtask * async_subtask)214 async_subtask_complete (AsyncSubtask *async_subtask)
215 {
216 GSimpleAsyncResult *simple;
217 AsyncContext *async_context;
218 gboolean cancel_subtasks = FALSE;
219 gdouble seconds_elapsed;
220
221 simple = async_subtask->simple;
222 async_context = g_simple_async_result_get_op_res_gpointer (simple);
223
224 g_mutex_lock (&async_context->lock);
225
226 seconds_elapsed = g_timer_elapsed (async_context->timer, NULL);
227
228 /* Discard successfully completed subtasks with no match found.
229 * Keep failed subtasks around so we have a GError to propagate
230 * if we need one, but those go on the end of the queue. */
231
232 if (async_subtask->stream != NULL) {
233 g_queue_insert_sorted (
234 &async_context->results,
235 async_subtask_ref (async_subtask),
236 (GCompareDataFunc) async_subtask_compare,
237 NULL);
238
239 /* If enough seconds have elapsed, just take the highest
240 * priority input stream we have. Cancel the unfinished
241 * subtasks and let them complete with an error. */
242 if (seconds_elapsed > ASYNC_TIMEOUT_SECONDS)
243 cancel_subtasks = TRUE;
244
245 } else if (async_subtask->error != NULL) {
246 g_queue_push_tail (
247 &async_context->results,
248 async_subtask_ref (async_subtask));
249 }
250
251 g_hash_table_remove (async_context->subtasks, async_subtask);
252
253 if (g_hash_table_size (async_context->subtasks) > 0) {
254 /* Let the remaining subtasks finish. */
255 goto exit;
256 }
257
258 /* The queue should be ordered now such that subtasks
259 * with input streams are before subtasks with errors.
260 * So just evaluate the first subtask on the queue. */
261
262 async_subtask = g_queue_pop_head (&async_context->results);
263
264 if (async_subtask != NULL) {
265 if (async_subtask->stream != NULL) {
266 async_context->stream =
267 g_converter_input_stream_new (
268 async_subtask->stream,
269 async_context->data_capture);
270 }
271
272 if (async_subtask->error != NULL) {
273 g_simple_async_result_take_error (
274 simple, async_subtask->error);
275 async_subtask->error = NULL;
276 }
277
278 async_subtask_unref (async_subtask);
279 }
280
281 g_simple_async_result_complete_in_idle (simple);
282
283 exit:
284 g_mutex_unlock (&async_context->lock);
285
286 if (cancel_subtasks) {
287 /* Call this after the mutex is unlocked. */
288 async_context_cancel_subtasks (async_context);
289 }
290 }
291
292 static void
async_context_cancelled_cb(GCancellable * cancellable,AsyncContext * async_context)293 async_context_cancelled_cb (GCancellable *cancellable,
294 AsyncContext *async_context)
295 {
296 async_context_cancel_subtasks (async_context);
297 }
298
299 static AsyncContext *
async_context_new(EDataCapture * data_capture,GCancellable * cancellable)300 async_context_new (EDataCapture *data_capture,
301 GCancellable *cancellable)
302 {
303 AsyncContext *async_context;
304
305 async_context = g_slice_new0 (AsyncContext);
306 g_mutex_init (&async_context->lock);
307 async_context->timer = g_timer_new ();
308
309 async_context->subtasks = g_hash_table_new_full (
310 (GHashFunc) g_direct_hash,
311 (GEqualFunc) g_direct_equal,
312 (GDestroyNotify) async_subtask_unref,
313 (GDestroyNotify) NULL);
314
315 async_context->data_capture = G_CONVERTER (g_object_ref (data_capture));
316
317 if (G_IS_CANCELLABLE (cancellable)) {
318 gulong handler_id;
319
320 async_context->cancellable = g_object_ref (cancellable);
321
322 handler_id = g_cancellable_connect (
323 async_context->cancellable,
324 G_CALLBACK (async_context_cancelled_cb),
325 async_context,
326 (GDestroyNotify) NULL);
327 async_context->cancelled_handler_id = handler_id;
328 }
329
330 return async_context;
331 }
332
333 static void
async_context_free(AsyncContext * async_context)334 async_context_free (AsyncContext *async_context)
335 {
336 /* Do this first so the callback won't fire
337 * while we're dismantling the AsyncContext. */
338 if (async_context->cancelled_handler_id > 0)
339 g_cancellable_disconnect (
340 async_context->cancellable,
341 async_context->cancelled_handler_id);
342
343 g_mutex_clear (&async_context->lock);
344 g_timer_destroy (async_context->timer);
345
346 g_hash_table_destroy (async_context->subtasks);
347
348 g_clear_object (&async_context->stream);
349 g_clear_object (&async_context->data_capture);
350 g_clear_object (&async_context->cancellable);
351
352 g_slice_free (AsyncContext, async_context);
353 }
354
355 static void
async_context_cancel_subtasks(AsyncContext * async_context)356 async_context_cancel_subtasks (AsyncContext *async_context)
357 {
358 GMainContext *main_context;
359 GList *list, *link;
360
361 main_context = g_main_context_ref_thread_default ();
362
363 g_mutex_lock (&async_context->lock);
364
365 list = g_hash_table_get_keys (async_context->subtasks);
366
367 /* XXX Cancel subtasks from idle callbacks to make sure we don't
368 * finalize the GSimpleAsyncResult during a "cancelled" signal
369 * emission from the main task's GCancellable. That will make
370 * g_cancellable_disconnect() in async_context_free() deadlock. */
371 for (link = list; link != NULL; link = g_list_next (link)) {
372 AsyncSubtask *async_subtask = link->data;
373 GSource *idle_source;
374
375 idle_source = g_idle_source_new ();
376 g_source_set_priority (idle_source, G_PRIORITY_HIGH_IDLE);
377 g_source_set_callback (
378 idle_source,
379 async_subtask_cancel_idle_cb,
380 async_subtask_ref (async_subtask),
381 (GDestroyNotify) async_subtask_unref);
382 g_source_attach (idle_source, main_context);
383 g_source_unref (idle_source);
384 }
385
386 g_list_free (list);
387
388 g_mutex_unlock (&async_context->lock);
389
390 g_main_context_unref (main_context);
391 }
392
393 static DataCaptureClosure *
data_capture_closure_new(EPhotoCache * photo_cache,const gchar * email_address)394 data_capture_closure_new (EPhotoCache *photo_cache,
395 const gchar *email_address)
396 {
397 DataCaptureClosure *closure;
398
399 closure = g_slice_new0 (DataCaptureClosure);
400 g_weak_ref_set (&closure->photo_cache, photo_cache);
401 closure->email_address = g_strdup (email_address);
402
403 return closure;
404 }
405
406 static void
data_capture_closure_free(DataCaptureClosure * closure)407 data_capture_closure_free (DataCaptureClosure *closure)
408 {
409 g_weak_ref_set (&closure->photo_cache, NULL);
410 g_free (closure->email_address);
411
412 g_slice_free (DataCaptureClosure, closure);
413 }
414
415 static PhotoData *
photo_data_new(GBytes * bytes)416 photo_data_new (GBytes *bytes)
417 {
418 PhotoData *photo_data;
419
420 photo_data = g_slice_new0 (PhotoData);
421 photo_data->ref_count = 1;
422 g_mutex_init (&photo_data->lock);
423
424 if (bytes != NULL)
425 photo_data->bytes = g_bytes_ref (bytes);
426
427 return photo_data;
428 }
429
430 static PhotoData *
photo_data_ref(PhotoData * photo_data)431 photo_data_ref (PhotoData *photo_data)
432 {
433 g_return_val_if_fail (photo_data != NULL, NULL);
434 g_return_val_if_fail (photo_data->ref_count > 0, NULL);
435
436 g_atomic_int_inc (&photo_data->ref_count);
437
438 return photo_data;
439 }
440
441 static void
photo_data_unref(PhotoData * photo_data)442 photo_data_unref (PhotoData *photo_data)
443 {
444 g_return_if_fail (photo_data != NULL);
445 g_return_if_fail (photo_data->ref_count > 0);
446
447 if (g_atomic_int_dec_and_test (&photo_data->ref_count)) {
448 g_mutex_clear (&photo_data->lock);
449 if (photo_data->bytes != NULL)
450 g_bytes_unref (photo_data->bytes);
451 g_slice_free (PhotoData, photo_data);
452 }
453 }
454
455 static GBytes *
photo_data_ref_bytes(PhotoData * photo_data)456 photo_data_ref_bytes (PhotoData *photo_data)
457 {
458 GBytes *bytes = NULL;
459
460 g_mutex_lock (&photo_data->lock);
461
462 if (photo_data->bytes != NULL)
463 bytes = g_bytes_ref (photo_data->bytes);
464
465 g_mutex_unlock (&photo_data->lock);
466
467 return bytes;
468 }
469
470 static void
photo_data_set_bytes(PhotoData * photo_data,GBytes * bytes)471 photo_data_set_bytes (PhotoData *photo_data,
472 GBytes *bytes)
473 {
474 g_mutex_lock (&photo_data->lock);
475
476 g_clear_pointer (&photo_data->bytes, g_bytes_unref);
477
478 if (bytes != NULL)
479 photo_data->bytes = g_bytes_ref (bytes);
480
481 g_mutex_unlock (&photo_data->lock);
482 }
483
484 static gchar *
photo_ht_normalize_key(const gchar * email_address)485 photo_ht_normalize_key (const gchar *email_address)
486 {
487 gchar *lowercase_email_address;
488 gchar *collation_key;
489
490 lowercase_email_address = g_utf8_strdown (email_address, -1);
491 collation_key = g_utf8_collate_key (lowercase_email_address, -1);
492 g_free (lowercase_email_address);
493
494 return collation_key;
495 }
496
497 static void
photo_ht_insert(EPhotoCache * photo_cache,const gchar * email_address,GBytes * bytes)498 photo_ht_insert (EPhotoCache *photo_cache,
499 const gchar *email_address,
500 GBytes *bytes)
501 {
502 GHashTable *photo_ht;
503 GQueue *photo_ht_keys;
504 PhotoData *photo_data;
505 gchar *key;
506
507 g_return_if_fail (email_address != NULL);
508
509 photo_ht = photo_cache->priv->photo_ht;
510 photo_ht_keys = &photo_cache->priv->photo_ht_keys;
511
512 key = photo_ht_normalize_key (email_address);
513
514 g_mutex_lock (&photo_cache->priv->photo_ht_lock);
515
516 photo_data = g_hash_table_lookup (photo_ht, key);
517
518 if (photo_data != NULL) {
519 GList *link;
520
521 /* Replace the old photo data if we have new photo
522 * data, otherwise leave the old photo data alone. */
523 if (bytes != NULL)
524 photo_data_set_bytes (photo_data, bytes);
525
526 /* Move the key to the head of the MRU queue. */
527 link = g_queue_find_custom (
528 photo_ht_keys, key,
529 (GCompareFunc) strcmp);
530 if (link != NULL) {
531 g_queue_unlink (photo_ht_keys, link);
532 g_queue_push_head_link (photo_ht_keys, link);
533 }
534 } else {
535 photo_data = photo_data_new (bytes);
536
537 g_hash_table_insert (
538 photo_ht, g_strdup (key),
539 photo_data_ref (photo_data));
540
541 /* Push the key to the head of the MRU queue. */
542 g_queue_push_head (photo_ht_keys, g_strdup (key));
543
544 /* Trim the cache if necessary. */
545 while (g_queue_get_length (photo_ht_keys) > MAX_CACHE_SIZE) {
546 gchar *oldest_key;
547
548 oldest_key = g_queue_pop_tail (photo_ht_keys);
549 g_hash_table_remove (photo_ht, oldest_key);
550 g_free (oldest_key);
551 }
552
553 photo_data_unref (photo_data);
554 }
555
556 /* Hash table and queue sizes should be equal at all times. */
557 g_warn_if_fail (
558 g_hash_table_size (photo_ht) ==
559 g_queue_get_length (photo_ht_keys));
560
561 g_mutex_unlock (&photo_cache->priv->photo_ht_lock);
562
563 g_free (key);
564 }
565
566 static gboolean
photo_ht_lookup(EPhotoCache * photo_cache,const gchar * email_address,GInputStream ** out_stream)567 photo_ht_lookup (EPhotoCache *photo_cache,
568 const gchar *email_address,
569 GInputStream **out_stream)
570 {
571 GHashTable *photo_ht;
572 PhotoData *photo_data;
573 gboolean found = FALSE;
574 gchar *key;
575
576 g_return_val_if_fail (email_address != NULL, FALSE);
577 g_return_val_if_fail (out_stream != NULL, FALSE);
578
579 photo_ht = photo_cache->priv->photo_ht;
580
581 key = photo_ht_normalize_key (email_address);
582
583 g_mutex_lock (&photo_cache->priv->photo_ht_lock);
584
585 photo_data = g_hash_table_lookup (photo_ht, key);
586
587 if (photo_data != NULL) {
588 GBytes *bytes;
589
590 bytes = photo_data_ref_bytes (photo_data);
591 if (bytes != NULL) {
592 *out_stream =
593 g_memory_input_stream_new_from_bytes (bytes);
594 g_bytes_unref (bytes);
595 } else {
596 *out_stream = NULL;
597 }
598 found = TRUE;
599 }
600
601 g_mutex_unlock (&photo_cache->priv->photo_ht_lock);
602
603 g_free (key);
604
605 return found;
606 }
607
608 static gboolean
photo_ht_remove(EPhotoCache * photo_cache,const gchar * email_address)609 photo_ht_remove (EPhotoCache *photo_cache,
610 const gchar *email_address)
611 {
612 GHashTable *photo_ht;
613 GQueue *photo_ht_keys;
614 gchar *key;
615 gboolean removed = FALSE;
616
617 g_return_val_if_fail (email_address != NULL, FALSE);
618
619 photo_ht = photo_cache->priv->photo_ht;
620 photo_ht_keys = &photo_cache->priv->photo_ht_keys;
621
622 key = photo_ht_normalize_key (email_address);
623
624 g_mutex_lock (&photo_cache->priv->photo_ht_lock);
625
626 if (g_hash_table_remove (photo_ht, key)) {
627 GList *link;
628
629 link = g_queue_find_custom (
630 photo_ht_keys, key,
631 (GCompareFunc) strcmp);
632 if (link != NULL) {
633 g_free (link->data);
634 g_queue_delete_link (photo_ht_keys, link);
635 removed = TRUE;
636 }
637 }
638
639 /* Hash table and queue sizes should be equal at all times. */
640 g_warn_if_fail (
641 g_hash_table_size (photo_ht) ==
642 g_queue_get_length (photo_ht_keys));
643
644 g_mutex_unlock (&photo_cache->priv->photo_ht_lock);
645
646 g_free (key);
647
648 return removed;
649 }
650
651 static void
photo_ht_remove_all(EPhotoCache * photo_cache)652 photo_ht_remove_all (EPhotoCache *photo_cache)
653 {
654 GHashTable *photo_ht;
655 GQueue *photo_ht_keys;
656
657 photo_ht = photo_cache->priv->photo_ht;
658 photo_ht_keys = &photo_cache->priv->photo_ht_keys;
659
660 g_mutex_lock (&photo_cache->priv->photo_ht_lock);
661
662 g_hash_table_remove_all (photo_ht);
663
664 while (!g_queue_is_empty (photo_ht_keys))
665 g_free (g_queue_pop_head (photo_ht_keys));
666
667 g_mutex_unlock (&photo_cache->priv->photo_ht_lock);
668 }
669
670 static void
photo_cache_data_captured_cb(EDataCapture * data_capture,GBytes * bytes,DataCaptureClosure * closure)671 photo_cache_data_captured_cb (EDataCapture *data_capture,
672 GBytes *bytes,
673 DataCaptureClosure *closure)
674 {
675 EPhotoCache *photo_cache;
676
677 photo_cache = g_weak_ref_get (&closure->photo_cache);
678
679 if (photo_cache != NULL) {
680 e_photo_cache_add_photo (
681 photo_cache, closure->email_address, bytes);
682 g_object_unref (photo_cache);
683 }
684 }
685
686 static void
photo_cache_async_subtask_done_cb(GObject * source_object,GAsyncResult * result,gpointer user_data)687 photo_cache_async_subtask_done_cb (GObject *source_object,
688 GAsyncResult *result,
689 gpointer user_data)
690 {
691 AsyncSubtask *async_subtask = user_data;
692
693 e_photo_source_get_photo_finish (
694 E_PHOTO_SOURCE (source_object),
695 result,
696 &async_subtask->stream,
697 &async_subtask->priority,
698 &async_subtask->error);
699
700 async_subtask_complete (async_subtask);
701 async_subtask_unref (async_subtask);
702 }
703
704 static void
photo_cache_set_client_cache(EPhotoCache * photo_cache,EClientCache * client_cache)705 photo_cache_set_client_cache (EPhotoCache *photo_cache,
706 EClientCache *client_cache)
707 {
708 g_return_if_fail (E_IS_CLIENT_CACHE (client_cache));
709 g_return_if_fail (photo_cache->priv->client_cache == NULL);
710
711 photo_cache->priv->client_cache = g_object_ref (client_cache);
712 }
713
714 static void
photo_cache_set_property(GObject * object,guint property_id,const GValue * value,GParamSpec * pspec)715 photo_cache_set_property (GObject *object,
716 guint property_id,
717 const GValue *value,
718 GParamSpec *pspec)
719 {
720 switch (property_id) {
721 case PROP_CLIENT_CACHE:
722 photo_cache_set_client_cache (
723 E_PHOTO_CACHE (object),
724 g_value_get_object (value));
725 return;
726 }
727
728 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
729 }
730
731 static void
photo_cache_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)732 photo_cache_get_property (GObject *object,
733 guint property_id,
734 GValue *value,
735 GParamSpec *pspec)
736 {
737 switch (property_id) {
738 case PROP_CLIENT_CACHE:
739 g_value_take_object (
740 value,
741 e_photo_cache_ref_client_cache (
742 E_PHOTO_CACHE (object)));
743 return;
744 }
745
746 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
747 }
748
749 static void
photo_cache_dispose(GObject * object)750 photo_cache_dispose (GObject *object)
751 {
752 EPhotoCachePrivate *priv;
753
754 priv = E_PHOTO_CACHE_GET_PRIVATE (object);
755
756 g_clear_object (&priv->client_cache);
757
758 photo_ht_remove_all (E_PHOTO_CACHE (object));
759
760 /* Chain up to parent's dispose() method. */
761 G_OBJECT_CLASS (e_photo_cache_parent_class)->dispose (object);
762 }
763
764 static void
photo_cache_finalize(GObject * object)765 photo_cache_finalize (GObject *object)
766 {
767 EPhotoCachePrivate *priv;
768
769 priv = E_PHOTO_CACHE_GET_PRIVATE (object);
770
771 g_main_context_unref (priv->main_context);
772
773 g_hash_table_destroy (priv->photo_ht);
774 g_hash_table_destroy (priv->sources_ht);
775
776 g_mutex_clear (&priv->photo_ht_lock);
777 g_mutex_clear (&priv->sources_ht_lock);
778
779 /* Chain up to parent's finalize() method. */
780 G_OBJECT_CLASS (e_photo_cache_parent_class)->finalize (object);
781 }
782
783 static void
photo_cache_constructed(GObject * object)784 photo_cache_constructed (GObject *object)
785 {
786 /* Chain up to parent's constructed() method. */
787 G_OBJECT_CLASS (e_photo_cache_parent_class)->constructed (object);
788
789 e_extensible_load_extensions (E_EXTENSIBLE (object));
790 }
791
792 static void
e_photo_cache_class_init(EPhotoCacheClass * class)793 e_photo_cache_class_init (EPhotoCacheClass *class)
794 {
795 GObjectClass *object_class;
796
797 g_type_class_add_private (class, sizeof (EPhotoCachePrivate));
798
799 object_class = G_OBJECT_CLASS (class);
800 object_class->set_property = photo_cache_set_property;
801 object_class->get_property = photo_cache_get_property;
802 object_class->dispose = photo_cache_dispose;
803 object_class->finalize = photo_cache_finalize;
804 object_class->constructed = photo_cache_constructed;
805
806 /**
807 * EPhotoCache:client-cache:
808 *
809 * Cache of shared #EClient instances.
810 **/
811 g_object_class_install_property (
812 object_class,
813 PROP_CLIENT_CACHE,
814 g_param_spec_object (
815 "client-cache",
816 "Client Cache",
817 "Cache of shared EClient instances",
818 E_TYPE_CLIENT_CACHE,
819 G_PARAM_READWRITE |
820 G_PARAM_CONSTRUCT_ONLY |
821 G_PARAM_STATIC_STRINGS));
822 }
823
824 static void
e_photo_cache_init(EPhotoCache * photo_cache)825 e_photo_cache_init (EPhotoCache *photo_cache)
826 {
827 GHashTable *photo_ht;
828 GHashTable *sources_ht;
829
830 photo_ht = g_hash_table_new_full (
831 (GHashFunc) g_str_hash,
832 (GEqualFunc) g_str_equal,
833 (GDestroyNotify) g_free,
834 (GDestroyNotify) photo_data_unref);
835
836 sources_ht = g_hash_table_new_full (
837 (GHashFunc) g_direct_hash,
838 (GEqualFunc) g_direct_equal,
839 (GDestroyNotify) g_object_unref,
840 (GDestroyNotify) NULL);
841
842 photo_cache->priv = E_PHOTO_CACHE_GET_PRIVATE (photo_cache);
843 photo_cache->priv->main_context = g_main_context_ref_thread_default ();
844 photo_cache->priv->photo_ht = photo_ht;
845 photo_cache->priv->sources_ht = sources_ht;
846
847 g_mutex_init (&photo_cache->priv->photo_ht_lock);
848 g_mutex_init (&photo_cache->priv->sources_ht_lock);
849 }
850
851 /**
852 * e_photo_cache_new:
853 * @client_cache: an #EClientCache
854 *
855 * Creates a new #EPhotoCache instance.
856 *
857 * Returns: an #EPhotoCache
858 **/
859 EPhotoCache *
e_photo_cache_new(EClientCache * client_cache)860 e_photo_cache_new (EClientCache *client_cache)
861 {
862 g_return_val_if_fail (E_IS_CLIENT_CACHE (client_cache), NULL);
863
864 return g_object_new (
865 E_TYPE_PHOTO_CACHE,
866 "client-cache", client_cache, NULL);
867 }
868
869 /**
870 * e_photo_cache_ref_client_cache:
871 * @photo_cache: an #EPhotoCache
872 *
873 * Returns the #EClientCache passed to e_photo_cache_new().
874 *
875 * The returned #EClientCache is referenced for thread-safety and must be
876 * unreferenced with g_object_unref() when finished with it.
877 *
878 * Returns: an #EClientCache
879 **/
880 EClientCache *
e_photo_cache_ref_client_cache(EPhotoCache * photo_cache)881 e_photo_cache_ref_client_cache (EPhotoCache *photo_cache)
882 {
883 g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), NULL);
884
885 return g_object_ref (photo_cache->priv->client_cache);
886 }
887
888 /**
889 * e_photo_cache_add_photo_source:
890 * @photo_cache: an #EPhotoCache
891 * @photo_source: an #EPhotoSource
892 *
893 * Adds @photo_source as a potential source of photos.
894 **/
895 void
e_photo_cache_add_photo_source(EPhotoCache * photo_cache,EPhotoSource * photo_source)896 e_photo_cache_add_photo_source (EPhotoCache *photo_cache,
897 EPhotoSource *photo_source)
898 {
899 GHashTable *sources_ht;
900
901 g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache));
902 g_return_if_fail (E_IS_PHOTO_SOURCE (photo_source));
903
904 sources_ht = photo_cache->priv->sources_ht;
905
906 g_mutex_lock (&photo_cache->priv->sources_ht_lock);
907
908 g_hash_table_add (sources_ht, g_object_ref (photo_source));
909
910 g_mutex_unlock (&photo_cache->priv->sources_ht_lock);
911 }
912
913 /**
914 * e_photo_cache_list_photo_sources:
915 * @photo_cache: an #EPhotoCache
916 *
917 * Returns a list of photo sources for @photo_cache.
918 *
919 * The sources returned in the list are referenced for thread-safety.
920 * They must each be unreferenced with g_object_unref() when finished
921 * with them. Free the returned list itself with g_list_free().
922 *
923 * An easy way to free the list property in one step is as follows:
924 *
925 * |[
926 * g_list_free_full (list, g_object_unref);
927 * ]|
928 *
929 * Returns: a sorted list of photo sources
930 **/
931 GList *
e_photo_cache_list_photo_sources(EPhotoCache * photo_cache)932 e_photo_cache_list_photo_sources (EPhotoCache *photo_cache)
933 {
934 GHashTable *sources_ht;
935 GList *list;
936
937 g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), NULL);
938
939 sources_ht = photo_cache->priv->sources_ht;
940
941 g_mutex_lock (&photo_cache->priv->sources_ht_lock);
942
943 list = g_hash_table_get_keys (sources_ht);
944 g_list_foreach (list, (GFunc) g_object_ref, NULL);
945
946 g_mutex_unlock (&photo_cache->priv->sources_ht_lock);
947
948 return list;
949 }
950
951 /**
952 * e_photo_cache_remove_photo_source:
953 * @photo_cache: an #EPhotoCache
954 * @photo_source: an #EPhotoSource
955 *
956 * Removes @photo_source as a potential source of photos.
957 *
958 * Returns: %TRUE if @photo_source was found and removed, %FALSE if not
959 **/
960 gboolean
e_photo_cache_remove_photo_source(EPhotoCache * photo_cache,EPhotoSource * photo_source)961 e_photo_cache_remove_photo_source (EPhotoCache *photo_cache,
962 EPhotoSource *photo_source)
963 {
964 GHashTable *sources_ht;
965 gboolean removed;
966
967 g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE);
968 g_return_val_if_fail (E_IS_PHOTO_SOURCE (photo_source), FALSE);
969
970 sources_ht = photo_cache->priv->sources_ht;
971
972 g_mutex_lock (&photo_cache->priv->sources_ht_lock);
973
974 removed = g_hash_table_remove (sources_ht, photo_source);
975
976 g_mutex_unlock (&photo_cache->priv->sources_ht_lock);
977
978 return removed;
979 }
980
981 /**
982 * e_photo_cache_add_photo:
983 * @photo_cache: an #EPhotoCache
984 * @email_address: an email address
985 * @bytes: a #GBytes containing photo data, or %NULL
986 *
987 * Adds a cache entry for @email_address, such that subsequent photo requests
988 * for @email_address will yield a #GMemoryInputStream loaded with @bytes
989 * without consulting available photo sources.
990 *
991 * The @bytes argument can also be %NULL to indicate no photo is available for
992 * @email_address. Subsequent photo requests for @email_address will yield no
993 * input stream.
994 *
995 * The entry may be removed without notice however, subject to @photo_cache's
996 * internal caching policy.
997 **/
998 void
e_photo_cache_add_photo(EPhotoCache * photo_cache,const gchar * email_address,GBytes * bytes)999 e_photo_cache_add_photo (EPhotoCache *photo_cache,
1000 const gchar *email_address,
1001 GBytes *bytes)
1002 {
1003 g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache));
1004 g_return_if_fail (email_address != NULL);
1005
1006 photo_ht_insert (photo_cache, email_address, bytes);
1007 }
1008
1009 /**
1010 * e_photo_cache_remove_photo:
1011 * @photo_cache: an #EPhotoCache
1012 * @email_address: an email address
1013 *
1014 * Removes the cache entry for @email_address, if such an entry exists.
1015 *
1016 * Returns: %TRUE if a cache entry was found and removed
1017 **/
1018 gboolean
e_photo_cache_remove_photo(EPhotoCache * photo_cache,const gchar * email_address)1019 e_photo_cache_remove_photo (EPhotoCache *photo_cache,
1020 const gchar *email_address)
1021 {
1022 g_return_val_if_fail (E_IS_PHOTO_CACHE (photo_cache), FALSE);
1023 g_return_val_if_fail (email_address != NULL, FALSE);
1024
1025 return photo_ht_remove (photo_cache, email_address);
1026 }
1027
1028 /**
1029 * e_photo_cache_get_photo_sync:
1030 * @photo_cache: an #EPhotoCache
1031 * @email_address: an email address
1032 * @cancellable: optional #GCancellable object, or %NULL
1033 * @out_stream: return location for a #GInputStream, or %NULL
1034 * @error: return location for a #GError, or %NULL
1035 *
1036 * Searches available photo sources for a photo associated with
1037 * @email_address.
1038 *
1039 * If a match is found, a #GInputStream from which to read image data is
1040 * returned through the @out_stream return location. If no match is found,
1041 * the @out_stream return location is set to %NULL.
1042 *
1043 * The return value indicates whether the search completed successfully,
1044 * not whether a match was found. If an error occurs, the function will
1045 * set @error and return %FALSE.
1046 *
1047 * Returns: whether the search completed successfully
1048 **/
1049 gboolean
e_photo_cache_get_photo_sync(EPhotoCache * photo_cache,const gchar * email_address,GCancellable * cancellable,GInputStream ** out_stream,GError ** error)1050 e_photo_cache_get_photo_sync (EPhotoCache *photo_cache,
1051 const gchar *email_address,
1052 GCancellable *cancellable,
1053 GInputStream **out_stream,
1054 GError **error)
1055 {
1056 EAsyncClosure *closure;
1057 GAsyncResult *result;
1058 gboolean success;
1059
1060 closure = e_async_closure_new ();
1061
1062 e_photo_cache_get_photo (
1063 photo_cache, email_address, cancellable,
1064 e_async_closure_callback, closure);
1065
1066 result = e_async_closure_wait (closure);
1067
1068 success = e_photo_cache_get_photo_finish (
1069 photo_cache, result, out_stream, error);
1070
1071 e_async_closure_free (closure);
1072
1073 return success;
1074 }
1075
1076 /**
1077 * e_photo_cache_get_photo:
1078 * @photo_cache: an #EPhotoCache
1079 * @email_address: an email address
1080 * @cancellable: optional #GCancellable object, or %NULL
1081 * @callback: a #GAsyncReadyCallback to call when the request is satisfied
1082 * @user_data: data to pass to the callback function
1083 *
1084 * Asynchronously searches available photo sources for a photo associated
1085 * with @email_address.
1086 *
1087 * When the operation is finished, @callback will be called. You can then
1088 * call e_photo_cache_get_photo_finish() to get the result of the operation.
1089 **/
1090 void
e_photo_cache_get_photo(EPhotoCache * photo_cache,const gchar * email_address,GCancellable * cancellable,GAsyncReadyCallback callback,gpointer user_data)1091 e_photo_cache_get_photo (EPhotoCache *photo_cache,
1092 const gchar *email_address,
1093 GCancellable *cancellable,
1094 GAsyncReadyCallback callback,
1095 gpointer user_data)
1096 {
1097 GSimpleAsyncResult *simple;
1098 AsyncContext *async_context;
1099 EDataCapture *data_capture;
1100 GInputStream *stream = NULL;
1101 GList *list, *link;
1102
1103 g_return_if_fail (E_IS_PHOTO_CACHE (photo_cache));
1104 g_return_if_fail (email_address != NULL);
1105
1106 /* This will be used to eavesdrop on the resulting input stream
1107 * for the purpose of adding the photo data to the photo cache. */
1108 data_capture = e_data_capture_new (photo_cache->priv->main_context);
1109
1110 g_signal_connect_data (
1111 data_capture, "finished",
1112 G_CALLBACK (photo_cache_data_captured_cb),
1113 data_capture_closure_new (photo_cache, email_address),
1114 (GClosureNotify) data_capture_closure_free, 0);
1115
1116 async_context = async_context_new (data_capture, cancellable);
1117
1118 simple = g_simple_async_result_new (
1119 G_OBJECT (photo_cache), callback,
1120 user_data, e_photo_cache_get_photo);
1121
1122 g_simple_async_result_set_check_cancellable (simple, cancellable);
1123
1124 g_simple_async_result_set_op_res_gpointer (
1125 simple, async_context, (GDestroyNotify) async_context_free);
1126
1127 /* Check if we have this email address already cached. */
1128 if (photo_ht_lookup (photo_cache, email_address, &stream)) {
1129 async_context->stream = stream; /* takes ownership */
1130 g_simple_async_result_complete_in_idle (simple);
1131 goto exit;
1132 }
1133
1134 list = e_photo_cache_list_photo_sources (photo_cache);
1135
1136 if (list == NULL) {
1137 g_simple_async_result_complete_in_idle (simple);
1138 goto exit;
1139 }
1140
1141 g_mutex_lock (&async_context->lock);
1142
1143 /* Dispatch a subtask for each photo source. */
1144 for (link = list; link != NULL; link = g_list_next (link)) {
1145 EPhotoSource *photo_source;
1146 AsyncSubtask *async_subtask;
1147
1148 photo_source = E_PHOTO_SOURCE (link->data);
1149 async_subtask = async_subtask_new (photo_source, simple);
1150
1151 g_hash_table_add (
1152 async_context->subtasks,
1153 async_subtask_ref (async_subtask));
1154
1155 e_photo_source_get_photo (
1156 photo_source, email_address,
1157 async_subtask->cancellable,
1158 photo_cache_async_subtask_done_cb,
1159 async_subtask_ref (async_subtask));
1160
1161 async_subtask_unref (async_subtask);
1162 }
1163
1164 g_mutex_unlock (&async_context->lock);
1165
1166 g_list_free_full (list, (GDestroyNotify) g_object_unref);
1167
1168 /* Check if we were cancelled while dispatching subtasks. */
1169 if (g_cancellable_is_cancelled (cancellable))
1170 async_context_cancel_subtasks (async_context);
1171
1172 exit:
1173 g_object_unref (simple);
1174 g_object_unref (data_capture);
1175 }
1176
1177 /**
1178 * e_photo_cache_get_photo_finish:
1179 * @photo_cache: an #EPhotoCache
1180 * @result: a #GAsyncResult
1181 * @out_stream: return location for a #GInputStream, or %NULL
1182 * @error: return location for a #GError, or %NULL
1183 *
1184 * Finishes the operation started with e_photo_cache_get_photo().
1185 *
1186 * If a match was found, a #GInputStream from which to read image data is
1187 * returned through the @out_stream return location. If no match was found,
1188 * the @out_stream return location is set to %NULL.
1189 *
1190 * The return value indicates whether the search completed successfully,
1191 * not whether a match was found. If an error occurred, the function will
1192 * set @error and return %FALSE.
1193 *
1194 * Returns: whether the search completed successfully
1195 **/
1196 gboolean
e_photo_cache_get_photo_finish(EPhotoCache * photo_cache,GAsyncResult * result,GInputStream ** out_stream,GError ** error)1197 e_photo_cache_get_photo_finish (EPhotoCache *photo_cache,
1198 GAsyncResult *result,
1199 GInputStream **out_stream,
1200 GError **error)
1201 {
1202 GSimpleAsyncResult *simple;
1203 AsyncContext *async_context;
1204
1205 g_return_val_if_fail (
1206 g_simple_async_result_is_valid (
1207 result, G_OBJECT (photo_cache),
1208 e_photo_cache_get_photo), FALSE);
1209
1210 simple = G_SIMPLE_ASYNC_RESULT (result);
1211 async_context = g_simple_async_result_get_op_res_gpointer (simple);
1212
1213 if (g_simple_async_result_propagate_error (simple, error))
1214 return FALSE;
1215
1216 if (out_stream != NULL) {
1217 if (async_context->stream != NULL)
1218 *out_stream = g_object_ref (async_context->stream);
1219 else
1220 *out_stream = NULL;
1221 }
1222
1223 return TRUE;
1224 }
1225
1226