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