Revert "build fix: Only depend on Gee 0.8.3, since 0.8.4 isn't released yet"
[platform/upstream/folks.git] / folks / individual-aggregator.vala
1 /*
2  * Copyright (C) 2010 Collabora Ltd.
3  * Copyright (C) 2012, 2013 Philip Withnall
4  *
5  * This library is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU Lesser General Public License as published by
7  * the Free Software Foundation, either version 2.1 of the License, or
8  * (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public License
16  * along with this library.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * Authors:
19  *       Travis Reitter <travis.reitter@collabora.co.uk>
20  *       Philip Withnall <philip@tecnocode.co.uk>
21  */
22
23 using Gee;
24 using GLib;
25
26 /**
27  * Errors from {@link IndividualAggregator}s.
28  */
29 public errordomain Folks.IndividualAggregatorError
30 {
31   /**
32    * Adding a {@link Persona} to a {@link PersonaStore} failed.
33    */
34   ADD_FAILED,
35
36   /**
37    * An operation which required the use of a writeable store failed because no
38    * writeable store was available.
39    *
40    * @since 0.1.13
41    */
42   [Deprecated (since = "0.6.2.1",
43       replacement = "IndividualAggregatorError.NO_PRIMARY_STORE")]
44   NO_WRITEABLE_STORE,
45
46   /**
47    * The {@link PersonaStore} was offline (ie, this is a temporary failure).
48    *
49    * @since 0.3.0
50    */
51   STORE_OFFLINE,
52
53   /**
54    * The {@link PersonaStore} did not support writing to a property which the
55    * user requested to write to, or which was necessary to write to for storing
56    * linking information.
57    *
58    * @since 0.6.2
59    */
60   PROPERTY_NOT_WRITEABLE,
61
62   /**
63    * An operation which required the use of a primary store failed because no
64    * primary store was available.
65    *
66    * @since 0.6.3
67    */
68   NO_PRIMARY_STORE,
69 }
70
71 /**
72  * Stores {@link Individual}s which have been created through
73  * aggregation of all the {@link Persona}s provided by the various
74  * {@link Backend}s.
75  *
76  * This is the main interface for client applications.
77  *
78  * Linking and unlinking of personas and individuals is performed entirely
79  * through the aggregator. Personas may be linked together to form individuals;
80  * for example, the personas which form ``individual1`` and ``individual2`` may
81  * be linked together with ``another_persona`` to give a new {@link Individual}:
82  * {{{
83  *   var personas = new HashSet<Persona> ();
84  *   personas.add_all (individual1.personas);
85  *   personas.add_all (individual2.personas);
86  *   personas.add (another_persona);
87  *   yield my_individual_aggregator.link_personas (personas);
88  * }}}
89  * The individuals which contained those personas will be removed when
90  * {@link IndividualAggregator.link_personas} is called. Any personas in those
91  * individuals which were not included in the linking call may end up implicitly
92  * linked to the new individual, or may be aggregated into other new
93  * individuals.
94  *
95  * For example, consider the situation where ``individual1`` contains two
96  * personas, ``persona1A`` and ``persona1B``; ``individual2`` contains one
97  * persona, ``persona2A``; and ``another_persona`` comes from ``individual3``,
98  * which also contains ``persona3A`` and ``persona3B``. Calling
99  * {@link IndividualAggregator.link_personas} on ``persona1A``, ``persona1B``,
100  * ``persona2A`` and ``another_persona`` will result in ``individual1`` and
101  * ``individual2`` being removed. A new {@link Individual} will be created
102  * containing all the personas passed to the linking function. It might also
103  * contain ``persona3A`` and ``persona3B``; or they might be in one or two other
104  * new individuals.
105  *
106  * An existing individual may be unlinked to form singleton
107  * individuals for each of its personas:
108  * {{{
109  *   yield my_individual_aggregator.unlink_individual (my_individual);
110  * }}}
111  *
112  * Note that to link two individuals together, their two sets of personas must
113  * be linked together. There is no API to directly link the individuals
114  * themselves, as conceptually folks links {@link Persona}s, not
115  * {@link Individual}s.
116  */
117 public class Folks.IndividualAggregator : Object
118 {
119   private BackendStore _backend_store;
120   private HashMap<string, PersonaStore> _stores;
121   private unowned PersonaStore? _primary_store = null;
122   private HashSet<Backend> _backends;
123   private HashMultiMap<string, Individual> _link_map;
124   private bool _linking_enabled = true;
125   private bool _is_prepared = false;
126   private bool _prepare_pending = false;
127   private Debug _debug;
128   private string _configured_primary_store_type_id;
129   private string _configured_primary_store_id;
130   private static const string _FOLKS_GSETTINGS_SCHEMA = "org.freedesktop.folks";
131   private static const string _PRIMARY_STORE_CONFIG_KEY = "primary-store";
132
133   /* The number of persona stores and backends we're waiting to become
134    * quiescent. Once these both reach 0, we should be in a quiescent state.
135    * We have to count both of them so that we can handle the case where one
136    * backend becomes available, and its persona stores all become quiescent,
137    * long before any other backend becomes available. In this case, we want
138    * the aggregator to signal that it's reached a quiescent state only once
139    * all the other backends have also become available. */
140   private uint _non_quiescent_persona_store_count = 0;
141   /* Same for backends. */
142   private uint _non_quiescent_backend_count = 0;
143   private bool _is_quiescent = false;
144   /* As a precaution against backends/persona stores which never reach
145    * quiescence (due to bugs), we implement a timeout after which we forcibly
146    * reach quiescence. */
147   private uint _quiescent_timeout_id = 0;
148
149   private static const uint _QUIESCENT_TIMEOUT = 30; /* seconds */
150
151   /* We use this to know if the primary PersonaStore has been explicitly
152    * set by the user (either via GSettings or an env variable). If that is the
153    * case, we don't want to override it with other PersonaStores that
154    * announce themselves as default (i.e.: default address book from e-d-s). */
155   private bool _user_configured_primary_store = false;
156
157   /**
158    * Whether {@link IndividualAggregator.prepare} has successfully completed for
159    * this aggregator.
160    *
161    * @since 0.3.0
162    */
163   public bool is_prepared
164     {
165       get { return this._is_prepared; }
166     }
167
168   /**
169    * Whether the aggregator has reached a quiescent state. This will happen at
170    * some point after {@link IndividualAggregator.prepare} has successfully
171    * completed for the aggregator. An aggregator is in a quiescent state when
172    * all the {@link PersonaStore}s listed by its backends have reached a
173    * quiescent state. Once it's reached a quiescent state, this property will
174    * never change again (from ``true`` to ``false``).
175    *
176    * It's guaranteed that this property's value will only ever change after
177    * {@link IndividualAggregator.is_prepared} has changed to ``true``.
178    *
179    * @since 0.6.2
180    */
181   public bool is_quiescent
182     {
183       get { return this._is_quiescent; }
184     }
185
186   /**
187    * Our configured primary (writeable) store.
188    *
189    * Which one to use is decided (in order or precedence)
190    * by:
191    *
192    * - the FOLKS_PRIMARY_STORE env var (mostly for debugging)
193    * - the GSettings key set in ``_PRIMARY_STORE_CONFIG_KEY`` (system set store)
194    * - going with the ``key-file`` or ``eds`` store as the fall-back option
195    *
196    * @since 0.5.0
197    */
198   public PersonaStore? primary_store
199     {
200       get { return this._primary_store; }
201     }
202
203   private Map<string, Individual> _individuals;
204   private Map<string, Individual> _individuals_ro;
205
206   /**
207    * A map from {@link Individual.id}s to their {@link Individual}s.
208    *
209    * This is the canonical set of {@link Individual}s provided by this
210    * IndividualAggregator.
211    *
212    * {@link Individual}s may be added or removed using
213    * {@link IndividualAggregator.add_persona_from_details} and
214    * {@link IndividualAggregator.remove_individual}, respectively.
215    *
216    * @since 0.5.1
217    */
218   public Map<string, Individual> individuals
219     {
220       get { return this._individuals_ro; }
221       private set
222         {
223           this._individuals = value;
224           this._individuals_ro = this._individuals.read_only_view;
225         }
226     }
227
228   /**
229    * The {@link Individual} representing the user.
230    *
231    * If it exists, this holds the {@link Individual} who is the user: the
232    * {@link Individual} containing the {@link Persona}s who are the owners of
233    * the accounts for their respective backends.
234    *
235    * @since 0.3.0
236    */
237   public Individual? user { get; private set; }
238
239   /**
240    * Emitted when one or more {@link Individual}s are added to or removed from
241    * the aggregator.
242    *
243    * If more information about the relationships between {@link Individual}s
244    * which have been linked and unlinked is needed, consider connecting to
245    * {@link IndividualAggregator.individuals_changed_detailed} instead, which is
246    * emitted at the same time as this signal.
247    *
248    * This will not be emitted until after {@link IndividualAggregator.prepare}
249    * has been called.
250    *
251    * @param added a list of {@link Individual}s which have been added
252    * @param removed a list of {@link Individual}s which have been removed
253    * @param message a string message from the backend, if any
254    * @param actor the {@link Persona} who made the change, if known
255    * @param reason the reason for the change
256    *
257    * @since 0.5.1
258    */
259   [Deprecated (since = "0.6.2",
260       replacement = "IndividualAggregator.individuals_changed_detailed")]
261   public signal void individuals_changed (Set<Individual> added,
262       Set<Individual> removed,
263       string? message,
264       Persona? actor,
265       GroupDetails.ChangeReason reason);
266
267   /**
268    * Emitted when one or more {@link Individual}s are added to or removed from
269    * the aggregator.
270    *
271    * This is emitted at the same time as
272    * {@link IndividualAggregator.individuals_changed}, but includes more
273    * information about the relationships between {@link Individual}s which have
274    * been linked and unlinked.
275    *
276    * Individuals which have been linked will be listed in the multi-map as
277    * mappings from the old individuals to the single new individual which
278    * replaces them (i.e. each of the old individuals will map to the same new
279    * individual). This new individual is the one which will be specified as the
280    * ``replacement_individual`` in the {@link Individual.removed} signal for the
281    * old individuals.
282    *
283    * Individuals which have been unlinked will be listed in the multi-map as
284    * a mapping from the unlinked individual to a set of one or more individuals
285    * which replace it.
286    *
287    * Individuals which have been added will be listed in the multi-map as a
288    * mapping from ``null`` to the set of added individuals. If ``null`` doesn't
289    * map to anything, no individuals have been added to the aggregator.
290    *
291    * Individuals which have been removed will be listed in the multi-map as
292    * mappings from the removed individual to ``null``.
293    *
294    * This will not be emitted until after {@link IndividualAggregator.prepare}
295    * has been called.
296    *
297    * @param changes a mapping of old {@link Individual}s to new
298    * {@link Individual}s for the individuals which have changed in the
299    * aggregator
300    *
301    * @since 0.6.2
302    */
303   public signal void individuals_changed_detailed (
304       MultiMap<Individual?, Individual?> changes);
305
306   /* FIXME: make this a singleton? */
307   /**
308    * Create a new IndividualAggregator.
309    *
310    * Clients should connect to the
311    * {@link IndividualAggregator.individuals_changed} signal (or the
312    * {@link IndividualAggregator.individuals_changed_detailed} signal), then
313    * call {@link IndividualAggregator.prepare} to load the backends and start
314    * aggregating individuals.
315    *
316    * An example of how to set up an IndividualAggregator:
317    * {{{
318    *   IndividualAggregator agg = new IndividualAggregator ();
319    *   agg.individuals_changed_detailed.connect (individuals_changed_cb);
320    *   agg.prepare ();
321    * }}}
322    */
323   public IndividualAggregator ()
324   {
325     Object ();
326     this._backend_store = BackendStore.dup ();
327   }
328   
329   /**
330    * Create a new IndividualAggregator with a custom {@link BackendStore}.
331    *
332    * This behaves the same as the default constructor for 
333    * {@link IndividualAggregator}, but uses the given {@link BackendStore}
334    * rather than the default one.
335    *
336    * @param store the {@link BackendStore} to use instead of the default one.
337    *
338    * @since UNRELEASED
339    */
340   public IndividualAggregator.with_backend_store (BackendStore store)
341   {
342     Object ();
343     this._backend_store = store;
344   }
345   
346   construct
347     {
348       this._stores = new HashMap<string, PersonaStore> ();
349       this._individuals = new HashMap<string, Individual> ();
350       this._individuals_ro = this._individuals.read_only_view;
351       this._link_map = new HashMultiMap<string, Individual> ();
352
353       this._backends = new HashSet<Backend> ();
354       this._debug = Debug.dup ();
355       this._debug.print_status.connect (this._debug_print_status);
356
357       /* Check out the configured primary store */
358       var store_config_ids = Environment.get_variable ("FOLKS_PRIMARY_STORE");
359       if (store_config_ids == null)
360         {
361           store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
362           if (store_config_ids != null)
363             {
364               var deprecated_warn = "FOLKS_WRITEABLE_STORE is deprecated, ";
365               deprecated_warn += "use FOLKS_PRIMARY_STORE";
366               warning (deprecated_warn);
367             }
368         }
369
370       if (store_config_ids != null)
371         {
372           debug ("Setting primary store IDs from environment variable.");
373           this._configure_primary_store ((!) store_config_ids);
374         }
375       else
376         {
377           debug ("Setting primary store IDs to defaults.");
378           if (BuildConf.HAVE_EDS)
379             {
380               this._configured_primary_store_type_id = "eds";
381               this._configured_primary_store_id = "system-address-book";
382             }
383           else
384             {
385               this._configured_primary_store_type_id = "key-file";
386               this._configured_primary_store_id = "";
387             }
388
389           var settings = new Settings (IndividualAggregator._FOLKS_GSETTINGS_SCHEMA);
390           var val = settings.get_string (IndividualAggregator._PRIMARY_STORE_CONFIG_KEY);
391           if (val != null && val != "")
392             {
393               debug ("Setting primary store IDs from GSettings.");
394               this._configure_primary_store ((!) val);
395             }
396         }
397
398       debug ("Primary store IDs are '%s' and '%s'.",
399           this._configured_primary_store_type_id,
400           this._configured_primary_store_id);
401
402       var disable_linking = Environment.get_variable ("FOLKS_DISABLE_LINKING");
403       if (disable_linking != null)
404         disable_linking = ((!) disable_linking).strip ().down ();
405       this._linking_enabled = (disable_linking == null ||
406           disable_linking == "no" || disable_linking == "0");
407
408       this._backend_store = BackendStore.dup ();
409
410       debug ("Constructing IndividualAggregator %p", this);
411     }
412
413   ~IndividualAggregator ()
414     {
415       debug ("Destroying IndividualAggregator %p", this);
416
417       if (this._quiescent_timeout_id != 0)
418         {
419           Source.remove (this._quiescent_timeout_id);
420           this._quiescent_timeout_id = 0;
421         }
422
423       this._backend_store.backend_available.disconnect (
424           this._backend_available_cb);
425
426       this._debug.print_status.disconnect (this._debug_print_status);
427     }
428
429   private void _configure_primary_store (string store_config_ids)
430     {
431       debug ("_configure_primary_store to '%s'", store_config_ids);
432       this._user_configured_primary_store = true;
433
434       if (store_config_ids.index_of (":") != -1)
435         {
436           var ids = store_config_ids.split (":", 2);
437           this._configured_primary_store_type_id = ids[0];
438           this._configured_primary_store_id = ids[1];
439         }
440       else
441         {
442           this._configured_primary_store_type_id = store_config_ids;
443           this._configured_primary_store_id = "";
444         }
445     }
446
447   private void _debug_print_status (Debug debug)
448     {
449       const string domain = Debug.STATUS_LOG_DOMAIN;
450       const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
451
452       debug.print_heading (domain, level, "IndividualAggregator (%p)", this);
453       debug.print_key_value_pairs (domain, level,
454           "Ref. count", this.ref_count.to_string (),
455           "Primary store", "%p".printf (this._primary_store),
456           "Configured store type id", this._configured_primary_store_type_id,
457           "Configured store id", this._configured_primary_store_id,
458           "Linking enabled?", this._linking_enabled ? "yes" : "no",
459           "Prepared?", this._is_prepared ? "yes" : "no",
460           "Quiescent?", this._is_quiescent
461               ? "yes"
462               : "no (%u backends, %u persona stores left)".printf (
463                   this._non_quiescent_backend_count,
464                   this._non_quiescent_persona_store_count)
465       );
466
467       debug.print_line (domain, level,
468           "%u Individuals:", this.individuals.size);
469       debug.indent ();
470
471       foreach (var individual in this.individuals.values)
472         {
473           string? trust_level = null;
474
475           switch (individual.trust_level)
476             {
477               case TrustLevel.NONE:
478                 trust_level = "none";
479                 break;
480               case TrustLevel.PERSONAS:
481                 trust_level = "personas";
482                 break;
483               default:
484                 assert_not_reached ();
485             }
486
487           debug.print_heading (domain, level, "Individual (%p)", individual);
488           debug.print_key_value_pairs (domain, level,
489               "Ref. count", individual.ref_count.to_string (),
490               "ID", individual.id,
491               "User?", individual.is_user ? "yes" : "no",
492               "Trust level", trust_level
493           );
494           debug.print_line (domain, level, "%u Personas:",
495               individual.personas.size);
496
497           debug.indent ();
498
499           foreach (var persona in individual.personas)
500             {
501               debug.print_heading (domain, level, "Persona (%p)", persona);
502               debug.print_key_value_pairs (domain, level,
503                   "Ref. count", persona.ref_count.to_string (),
504                   "UID", persona.uid,
505                   "IID", persona.iid,
506                   "Display ID", persona.display_id,
507                   "User?", persona.is_user ? "yes" : "no"
508               );
509             }
510
511           debug.unindent ();
512         }
513
514       debug.unindent ();
515
516       debug.print_line (domain, level, "%u entries in the link map:",
517           this._link_map.size);
518       debug.indent ();
519
520       foreach (var link_key in this._link_map.get_keys ())
521         {
522           debug.print_line (domain, level, "%s → {", link_key);
523           debug.indent ();
524
525           foreach (var individual in this._link_map.get (link_key))
526             {
527               debug.print_line (domain, level, "%p", individual);
528             }
529
530           debug.unindent ();
531           debug.print_line (domain, level, "}");
532         }
533
534       debug.unindent ();
535
536       debug.print_line (domain, level, "");
537     }
538
539   /**
540    * Prepare the IndividualAggregator for use.
541    *
542    * This loads all the available backends and prepares them for use by the
543    * IndividualAggregator. This should be called //after// connecting to the
544    * {@link IndividualAggregator.individuals_changed} signal (or
545    * {@link IndividualAggregator.individuals_changed_detailed} signal), or a
546    * race condition could occur, with the signal being emitted before your code
547    * has connected to them, and {@link Individual}s getting "lost" as a result.
548    *
549    * This function is guaranteed to be idempotent (since version 0.3.0).
550    *
551    * Concurrent calls to this function from different threads will block until
552    * preparation has completed. However, concurrent calls to this function from
553    * a single thread might not, i.e. the first call will block but subsequent
554    * calls might return before the first one. (Though they will be safe in every
555    * other respect.)
556    *
557    * @throws GLib.Error if preparing any of the backends failed — this error
558    * will be passed through from {@link BackendStore.load_backends}
559    *
560    * @since 0.1.11
561    */
562   public async void prepare () throws GLib.Error
563     {
564       Internal.profiling_start ("preparing IndividualAggregator");
565
566       /* Once this async function returns, all the {@link Backend}s will have
567        * been prepared (though no {@link PersonaStore}s are guaranteed to be
568        * available yet). This last guarantee is new as of version 0.2.0. */
569
570       if (this._is_prepared || this._prepare_pending)
571         {
572           return;
573         }
574
575       try
576         {
577           this._prepare_pending = true;
578
579           /* Temporarily increase the non-quiescent backend count so that
580            * we don't prematurely reach quiescence due to odd timing of the
581            * backend-available signals. */
582           this._non_quiescent_backend_count++;
583
584           this._backend_store.backend_available.connect (
585               this._backend_available_cb);
586
587           /* Load any backends which already exist. This could happen if the
588            * BackendStore has stayed alive after being used by a previous
589            * IndividualAggregator instance. */
590           var backends = this._backend_store.enabled_backends.values;
591           foreach (var backend in backends)
592             {
593               this._backend_available_cb (this._backend_store, backend);
594             }
595
596           /* Load any backends which haven't been loaded already. (Typically
597            * all of them.) */
598           yield this._backend_store.load_backends ();
599
600           this._non_quiescent_backend_count--;
601
602           this._is_prepared = true;
603           this._prepare_pending = false;
604           this.notify_property ("is-prepared");
605
606           /* Mark the aggregator as having reached a quiescent state if
607            * appropriate. This will typically only happen here in cases
608            * where the stores were all prepared and quiescent before the
609            * aggregator was created. */
610           if (this._is_quiescent == false)
611             {
612               this._notify_if_is_quiescent ();
613             }
614         }
615       finally
616         {
617           this._prepare_pending = false;
618         }
619
620       Internal.profiling_end ("preparing IndividualAggregator");
621     }
622
623   /**
624    * Clean up and release resources used by the aggregator.
625    *
626    * This will disconnect the aggregator cleanly from any resources it or its
627    * persona stores are using. It is recommended to call this method before
628    * finalising the individual aggregator, but calling it is not required. If
629    * this method is not called then, for example, unsaved changes in backends
630    * may not be flushed.
631    *
632    * Concurrent calls to this function from different threads will block until
633    * preparation has completed. However, concurrent calls to this function from
634    * a single thread might not, i.e. the first call will block but subsequent
635    * calls might return before the first one. (Though they will be safe in every
636    * other respect.)
637    *
638    * @since 0.7.3
639    * @throws GLib.Error if unpreparing the backend-specific services failed —
640    * this will be a backend-specific error
641    */
642   public async void unprepare () throws GLib.Error
643     {
644       lock (this._is_prepared)
645         {
646           if (!this._is_prepared || this._prepare_pending)
647             {
648               return;
649             }
650
651           try
652             {
653               /* Flush any PersonaStores which need it. */
654               foreach (var p in this._stores.values)
655                 {
656                   yield p.flush ();
657                 }
658             }
659           finally
660             {
661               this._prepare_pending = false;
662             }
663         }
664     }
665
666   /**
667    * Get all matches for a given {@link Individual}.
668    *
669    * @param matchee the individual to find matches for
670    * @param min_threshold the threshold for accepting a match
671    * @return a map from matched individuals to the degree with which they match
672    * ``matchee`` (which is guaranteed to at least equal ``min_threshold``);
673    * if no matches could be found, an empty map is returned
674    *
675    * @since 0.5.1
676    */
677   public Map<Individual, MatchResult> get_potential_matches
678       (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
679     {
680       HashMap<Individual, MatchResult> matches =
681           new HashMap<Individual, MatchResult> ();
682       Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
683
684       foreach (var i in this._individuals.values)
685         {
686           if (i.id == matchee.id)
687                 continue;
688
689           var result = matchObj.potential_match (i, matchee);
690           if (result >= min_threshold)
691             {
692               matches.set (i, result);
693             }
694         }
695
696       return matches;
697     }
698
699   /**
700    * Get all combinations between all {@link Individual}s.
701    *
702    * @param min_threshold the threshold for accepting a match
703    * @return a map from each individual in the aggregator to a map of the
704    * other individuals in the aggregator which can be matched with that
705    * individual, mapped to the degree with which they match the original
706    * individual (which is guaranteed to at least equal ``min_threshold``)
707    *
708    * @since 0.5.1
709    */
710   public Map<Individual, Map<Individual, MatchResult>>
711       get_all_potential_matches
712         (MatchResult min_threshold = MatchResult.VERY_HIGH)
713     {
714       HashMap<Individual, HashMap<Individual, MatchResult>> matches =
715         new HashMap<Individual, HashMap<Individual, MatchResult>> ();
716       var individuals = this._individuals.values.to_array ();
717       Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
718
719       for (var i = 0; i < individuals.length; i++)
720         {
721           var a = individuals[i];
722
723           HashMap<Individual, MatchResult>? _matches_a = matches.get (a);
724           HashMap<Individual, MatchResult> matches_a;
725           if (_matches_a == null)
726             {
727               matches_a = new HashMap<Individual, MatchResult> ();
728               matches.set (a, matches_a);
729             }
730           else
731             {
732               matches_a = (!) _matches_a;
733             }
734
735           for (var f = i + 1; f < individuals.length; f++)
736             {
737               var b = individuals[f];
738
739               HashMap<Individual, MatchResult>? _matches_b = matches.get (b);
740               HashMap<Individual, MatchResult> matches_b;
741               if (_matches_b == null)
742                 {
743                   matches_b = new HashMap<Individual, MatchResult> ();
744                   matches.set (b, matches_b);
745                 }
746               else
747                 {
748                   matches_b = (!) _matches_b;
749                 }
750
751               var result = matchObj.potential_match (a, b);
752
753               if (result >= min_threshold)
754                 {
755                   matches_a.set (b, result);
756                   matches_b.set (a, result);
757                 }
758             }
759         }
760
761       return matches;
762     }
763
764   private async void _add_backend (Backend backend)
765     {
766       if (!this._backends.contains (backend))
767         {
768           this._backends.add (backend);
769
770           backend.persona_store_added.connect (
771               this._backend_persona_store_added_cb);
772           backend.persona_store_removed.connect (
773               this._backend_persona_store_removed_cb);
774           backend.notify["is-quiescent"].connect (
775               this._backend_is_quiescent_changed_cb);
776
777           /* Handle the stores that have already been signaled. Since
778            * this might change while we are looping, get a copy first.
779            */
780           var stores = backend.persona_stores.values.to_array ();
781           foreach (var persona_store in stores)
782               {
783                 this._backend_persona_store_added_cb (backend, persona_store);
784               }
785         }
786     }
787
788   private void _backend_available_cb (BackendStore backend_store,
789       Backend backend)
790     {
791       /* Increase the number of non-quiescent backends we're waiting for.
792        * If we've already reached a quiescent state, this is ignored. If we
793        * haven't, this delays us reaching a quiescent state until the
794        * _backend_is_quiescent_changed_cb() callback is called for this
795        * backend. */
796       if (backend.is_quiescent == false)
797         {
798           this._non_quiescent_backend_count++;
799
800           /* Start the timeout to force quiescence if the backend (or its
801            * persona stores) misbehave and don't reach quiescence. */
802           if (this._quiescent_timeout_id == 0)
803             {
804               this._quiescent_timeout_id =
805                   Timeout.add_seconds (IndividualAggregator._QUIESCENT_TIMEOUT,
806                       this._quiescent_timeout_cb);
807             }
808         }
809
810       this._add_backend.begin (backend);
811     }
812
813   private void _set_primary_store (PersonaStore store)
814     {
815       debug ("_set_primary_store()");
816
817       if (this._primary_store == store)
818         return;
819
820       /* We use the configured PersonaStore as the primary PersonaStore.
821        *
822        * If the type_id is ``eds`` we *must* know the actual store
823        * (address book) we are talking about or we might end up using
824        * a random store on every run.
825        */
826       if (store.type_id == this._configured_primary_store_type_id)
827         {
828           if ((store.type_id != "eds" &&
829                   this._configured_primary_store_id == "") ||
830               this._configured_primary_store_id == store.id)
831             {
832               debug ("Setting primary store to %p (type ID: %s, ID: %s)",
833                   store, store.type_id, store.id);
834
835               var previous_store = this._primary_store;
836               this._primary_store = store;
837
838               store.freeze_notify ();
839               if (previous_store != null)
840                 {
841                   ((!) previous_store).freeze_notify ();
842                   ((!) previous_store).is_primary_store = false;
843                 }
844               store.is_primary_store = true;
845               if (previous_store != null)
846                 ((!) previous_store).thaw_notify ();
847               store.thaw_notify ();
848
849               this.notify_property ("primary-store");
850             }
851         }
852     }
853
854   private void _backend_persona_store_added_cb (Backend backend,
855       PersonaStore store)
856     {
857       debug ("_backend_persona_store_added_cb(): backend: %s, store: %s (%p)",
858           backend.name, store.id, store);
859
860       var store_id = this._get_store_full_id (store.type_id, store.id);
861
862       this._maybe_configure_as_primary (store);
863       this._set_primary_store (store);
864
865       this._stores.set (store_id, store);
866       store.personas_changed.connect (this._personas_changed_cb);
867       store.notify["is-primary-store"].connect (
868           this._is_primary_store_changed_cb);
869       store.notify["is-quiescent"].connect (
870           this._persona_store_is_quiescent_changed_cb);
871       store.notify["is-user-set-default"].connect (
872           this._persona_store_is_user_set_default_changed_cb);
873
874       /* Increase the number of non-quiescent persona stores we're waiting for.
875        * If we've already reached a quiescent state, this is ignored. If we
876        * haven't, this delays us reaching a quiescent state until the
877        * _persona_store_is_quiescent_changed_cb() callback is called for this
878        * store. */
879       if (store.is_quiescent == false)
880         {
881           this._non_quiescent_persona_store_count++;
882
883           /* Start the timeout to force quiescence if the backend (or its
884            * persona stores) misbehave and don't reach quiescence. */
885           if (this._quiescent_timeout_id == 0)
886             {
887               this._quiescent_timeout_id =
888                   Timeout.add_seconds (IndividualAggregator._QUIESCENT_TIMEOUT,
889                       this._quiescent_timeout_cb);
890             }
891         }
892
893       /* Handle any pre-existing personas in the store. This can happen if the
894        * store existed (and was prepared) before this IndividualAggregator was
895        * constructed. */
896       if (store.personas.size > 0)
897         {
898           var persona_set = new HashSet<Persona> ();
899           foreach (var p in store.personas.values)
900             {
901               persona_set.add (p);
902             }
903
904           this._personas_changed_cb (store, persona_set,
905               new HashSet<Persona> (), null, null,
906               GroupDetails.ChangeReason.NONE);
907         }
908
909       /* Prepare the store and receive a load of other personas-changed
910        * signals. */
911       store.prepare.begin ((obj, result) =>
912         {
913           try
914             {
915               store.prepare.end (result);
916             }
917           catch (GLib.Error e)
918             {
919               /* Translators: the first parameter is a persona store identifier
920                * and the second is an error message. */
921               warning (_("Error preparing persona store '%s': %s"), store_id,
922                   e.message);
923             }
924         });
925     }
926
927   private void _backend_persona_store_removed_cb (Backend backend,
928       PersonaStore store)
929     {
930       store.personas_changed.disconnect (this._personas_changed_cb);
931       store.notify["is-quiescent"].disconnect (
932           this._persona_store_is_quiescent_changed_cb);
933       store.notify["is-primary-store"].disconnect (
934           this._is_primary_store_changed_cb);
935       store.notify["is-user-set-default"].disconnect (
936           this._persona_store_is_user_set_default_changed_cb);
937
938       /* If we were still waiting on this persona store to reach a quiescent
939        * state, stop waiting. */
940       if (this._is_quiescent == false && store.is_quiescent == false)
941         {
942           this._non_quiescent_persona_store_count--;
943           this._notify_if_is_quiescent ();
944         }
945
946       /* Not all stores emit a 'removed' signal under all circumstances.
947        * The EDS backend doesn't do it when set_persona_stores() or disable_store()
948        * are used to disable a store.
949        * Therefore remove this store's personas from all the individuals. Should
950        * not have any effect if a store already triggered the 'removed' signals,
951        * because then we won't have anything here.
952        * See https://bugzilla.gnome.org/show_bug.cgi?id=689146
953        */
954
955       var removed_personas = new HashSet<Persona> ();
956       var iter = store.personas.map_iterator ();
957
958       while (iter.next () == true)
959         {
960           removed_personas.add (iter.get_value ());
961         }
962       this._personas_changed_cb (store, new HashSet<Persona> (), removed_personas,
963           null, null, GroupDetails.ChangeReason.NONE);
964
965       if (this._primary_store == store)
966         {
967           debug ("Unsetting primary store as store %p (type ID: %s, ID: %s) " +
968               "has been removed", store, store.type_id, store.id);
969           this._primary_store = null;
970           this.notify_property ("primary-store");
971         }
972       this._stores.unset (this._get_store_full_id (store.type_id, store.id));
973     }
974
975   private string _get_store_full_id (string type_id, string id)
976     {
977       return type_id + ":" + id;
978     }
979
980   /* Emit the individuals-changed signal ensuring that null parameters are
981    * turned into empty sets, and both sets passed to signal handlers are
982    * read-only. */
983   private void _emit_individuals_changed (Set<Individual>? added,
984       Set<Individual>? removed,
985       MultiMap<Individual?, Individual?>? changes,
986       string? message = null,
987       Persona? actor = null,
988       GroupDetails.ChangeReason reason = GroupDetails.ChangeReason.NONE)
989     {
990       Set<Individual> _added;
991       Set<Individual> _removed;
992       MultiMap<Individual?, Individual?> _changes;
993
994       if ((added == null || ((!) added).size == 0) &&
995           (removed == null || ((!) removed).size == 0) &&
996           (changes == null || ((!) changes).size == 0))
997         {
998           /* Don't bother emitting it if nothing's changed */
999           return;
1000         }
1001
1002       Internal.profiling_point ("emitting " +
1003           "IndividualAggregator::individuals-changed");
1004
1005       _added = (added != null) ? (!) added : new HashSet<Individual> ();
1006       _removed = (removed != null) ? (!) removed : new HashSet<Individual> ();
1007
1008       if (changes != null)
1009         {
1010           _changes = (!) changes;
1011         }
1012       else
1013         {
1014           _changes = new HashMultiMap<Individual?, Individual?> ();
1015         }
1016
1017       /* Debug output. */
1018       if (this._debug.debug_output_enabled == true)
1019         {
1020           debug ("Emitting individuals-changed-detailed with %u mappings:",
1021               _changes.size);
1022
1023           foreach (var removed_ind in _changes.get_keys ())
1024             {
1025               foreach (var added_ind in _changes.get (removed_ind))
1026                 {
1027                   debug ("    %s (%p) → %s (%p)",
1028                       (removed_ind != null) ? ((!) removed_ind).id : "",
1029                       removed_ind,
1030                       (added_ind != null) ? ((!) added_ind).id : "", added_ind);
1031
1032                   if (removed_ind != null)
1033                     {
1034                       debug ("      Removed individual's personas:");
1035
1036                       foreach (var p in ((!) removed_ind).personas)
1037                         {
1038                           debug ("        %s (%p)", p.uid, p);
1039                         }
1040                     }
1041
1042                   if (added_ind != null)
1043                     {
1044                       debug ("      Added individual's personas:");
1045
1046                       foreach (var p in ((!) added_ind).personas)
1047                         {
1048                           debug ("        %s (%p)", p.uid, p);
1049                         }
1050                     }
1051                 }
1052             }
1053         }
1054
1055       this.individuals_changed (_added.read_only_view, _removed.read_only_view,
1056           message, actor, reason);
1057       this.individuals_changed_detailed (_changes);
1058     }
1059
1060   private void _connect_to_individual (Individual individual)
1061     {
1062       individual.removed.connect (this._individual_removed_cb);
1063       this._individuals.set (individual.id, individual);
1064     }
1065
1066   private void _disconnect_from_individual (Individual individual)
1067     {
1068       this._individuals.unset (individual.id);
1069       individual.removed.disconnect (this._individual_removed_cb);
1070     }
1071
1072   private void _add_personas (Set<Persona> added, ref Individual? user,
1073       ref HashMultiMap<Individual?, Individual?> individuals_changes)
1074     {
1075       foreach (var persona in added)
1076         {
1077           PersonaStoreTrust trust_level = persona.store.trust_level;
1078
1079           /* These are the Individuals whose Personas will be linked together
1080            * to form the ``final_individual``.
1081            * Since a given Persona can only be part of one Individual, and the
1082            * code in Persona._set_personas() ensures that there are no duplicate
1083            * Personas in a given Individual, ensuring that there are no
1084            * duplicate Individuals in ``candidate_inds`` (by using a
1085            * HashSet) guarantees that there will be no duplicate Personas
1086            * in the ``final_individual``. */
1087           HashSet<Individual> candidate_inds = new HashSet<Individual> ();
1088
1089           var final_personas = new HashSet<Persona> ();
1090
1091           debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
1092
1093           /* If the Persona is the user, we *always* want to link it to the
1094            * existing this.user. */
1095           if (persona.is_user == true && user != null &&
1096               ((!) user).has_anti_link_with_persona (persona) == false)
1097             {
1098               debug ("    Found candidate individual '%s' as user.",
1099                   ((!) user).id);
1100               candidate_inds.add ((!) user);
1101             }
1102
1103           /* If we don't trust the PersonaStore at all, we can't link the
1104            * Persona to any existing Individual */
1105           if (trust_level != PersonaStoreTrust.NONE)
1106             {
1107               var candidate_ind_set = this._link_map.get (persona.iid);
1108               if (candidate_ind_set != null)
1109                 {
1110                   foreach (var candidate_ind in candidate_ind_set)
1111                     {
1112                       if (candidate_ind != null &&
1113                           ((!) candidate_ind).trust_level != TrustLevel.NONE &&
1114                           ((!) candidate_ind).has_anti_link_with_persona (
1115                               persona) == false &&
1116                           candidate_inds.add ((!) candidate_ind))
1117                         {
1118                           debug ("    Found candidate individual '%s' by " +
1119                               "IID '%s'.", ((!) candidate_ind).id, persona.iid);
1120                         }
1121                     }
1122                 }
1123             }
1124
1125           if (persona.store.trust_level == PersonaStoreTrust.FULL)
1126             {
1127               /* If we trust the PersonaStore the Persona came from, we can
1128                * attempt to link based on its linkable properties. */
1129               foreach (unowned string foo in persona.linkable_properties)
1130                 {
1131                   /* FIXME: If we just use string prop_name directly in the
1132                    * foreach, Vala doesn't copy it into the closure data, and
1133                    * prop_name ends up as NULL. bgo#628336 */
1134                   unowned string prop_name = foo;
1135
1136                   /* FIXME: can't be var because of bgo#638208 */
1137                   unowned ObjectClass pclass = persona.get_class ();
1138                   if (pclass.find_property (prop_name) == null)
1139                     {
1140                       warning (
1141                           /* Translators: the parameter is a property name. */
1142                           _("Unknown property '%s' in linkable property list."),
1143                           prop_name);
1144                       continue;
1145                     }
1146
1147                   persona.linkable_property_to_links (prop_name, (l) =>
1148                     {
1149                       unowned string prop_linking_value = l;
1150                       var candidate_ind_set =
1151                           this._link_map.get (prop_linking_value);
1152
1153                       if (candidate_ind_set != null)
1154                         {
1155                           foreach (var candidate_ind in candidate_ind_set)
1156                             {
1157                               if (candidate_ind != null &&
1158                                   ((!) candidate_ind).trust_level !=
1159                                       TrustLevel.NONE &&
1160                                   ((!) candidate_ind).
1161                                       has_anti_link_with_persona (
1162                                           persona) == false &&
1163                                   candidate_inds.add ((!) candidate_ind))
1164                                 {
1165                                   debug ("    Found candidate individual '%s'" +
1166                                       " by linkable property '%s' = '%s'.",
1167                                       ((!) candidate_ind).id, prop_name,
1168                                       prop_linking_value);
1169                                 }
1170                             }
1171                         }
1172                     });
1173                 }
1174             }
1175
1176           /* Ensure the original persona makes it into the final individual */
1177           final_personas.add (persona);
1178
1179           if (candidate_inds.size > 0 && this._linking_enabled == true)
1180             {
1181               /* The Persona's IID or linkable properties match one or more
1182                * linkable fields which are already in the link map, so we link
1183                * together all the Individuals we found to form a new
1184                * final_individual. Later, we remove the Personas from the old
1185                * Individuals so that the Individuals themselves are removed. */
1186               foreach (var individual in candidate_inds)
1187                 {
1188                   final_personas.add_all (individual.personas);
1189                 }
1190             }
1191           else if (candidate_inds.size > 0)
1192             {
1193               debug ("    Linking disabled.");
1194             }
1195           else
1196             {
1197               debug ("    Did not find any candidate individuals.");
1198             }
1199
1200           /* Create the final linked Individual */
1201           var final_individual = new Individual (final_personas);
1202           debug ("    Created new individual '%s' (%p) with personas:",
1203               final_individual.id, final_individual);
1204           foreach (var p in final_personas)
1205             {
1206               debug ("        %s (%p)", p.uid, p);
1207               this._add_persona_to_link_map (p, final_individual);
1208             }
1209
1210           uint num_mappings_added = 0;
1211
1212           foreach (var i in candidate_inds)
1213             {
1214               /* Remove the old individuals from the link map. */
1215               this._remove_individual_from_link_map (i);
1216
1217               /* Transitively update the individuals_changes. We have to do this
1218                * in two stages as we can't modify individuals_changes while
1219                * iterating over it. */
1220               var transitive_updates = new HashSet<Individual?> ();
1221
1222               foreach (var k in individuals_changes.get_keys ())
1223                 {
1224                   if (i in individuals_changes.get (k))
1225                     {
1226                       transitive_updates.add (k);
1227                     }
1228                 }
1229
1230               foreach (var k in transitive_updates)
1231                 {
1232                   assert (individuals_changes.remove (k, i) == true);
1233
1234                   /* If we're saying the final_individual is replacing some of
1235                    * these candidate individuals, we don't also want to say that
1236                    * it's been added (by also emitting a mapping from
1237                    * null → final_individual). */
1238                   if (k != null)
1239                     {
1240                       individuals_changes.set (k, final_individual);
1241                       num_mappings_added++;
1242                     }
1243                 }
1244
1245               /* If there were no transitive changes to make, it's because this
1246                * candidate individual existed before this call to
1247                * _add_personas(), so it's safe to say it's being replaced by
1248                * the final_individual. */
1249               if (transitive_updates.size == 0)
1250                 {
1251                   individuals_changes.set (i, final_individual);
1252                   num_mappings_added++;
1253                 }
1254             }
1255
1256           /* If there were no candidate individuals or they were all freshly
1257            * added (i.e. mapped from null → candidate_individual), mark the
1258            * final_individual as added. */
1259           if (num_mappings_added == 0)
1260             {
1261               individuals_changes.set (null, final_individual);
1262             }
1263
1264           /* If the final Individual is the user, set them as such. */
1265           if (final_individual.is_user == true)
1266             user = final_individual;
1267         }
1268     }
1269
1270   private void _persona_linkable_property_changed_cb (Object obj,
1271       ParamSpec pspec)
1272     {
1273       /* The value of one of the linkable properties of one the personas has
1274        * changed, so that persona might require re-linking. We do this in a
1275        * simplistic and hacky way (which should work) by simply treating the
1276        * persona as if it's been removed and re-added. */
1277       var persona = (!) (obj as Persona);
1278
1279       debug ("Linkable property '%s' changed for persona '%s' " +
1280           "(is user: %s, IID: %s).", pspec.name, persona.uid,
1281           persona.is_user ? "yes" : "no", persona.iid);
1282
1283       var persona_set = new HashSet<Persona> ();
1284       persona_set.add (persona);
1285
1286       this._personas_changed_cb (persona.store, persona_set, persona_set,
1287           null, null, GroupDetails.ChangeReason.NONE);
1288     }
1289
1290   private void _persona_anti_links_changed_cb (Object obj, ParamSpec pspec)
1291     {
1292       var persona = obj as Persona;
1293
1294       /* The anti-links associated with the persona has changed, so that persona
1295        * might require re-linking. We do this in a simplistic and hacky way
1296        * (which should work) by simply treating the persona as if it's been
1297        * removed and re-added. */
1298       debug ("Anti-links changed for persona '%s' (is user: %s, IID: %s).",
1299           persona.uid, persona.is_user ? "yes" : "no", persona.iid);
1300
1301       var persona_set = new HashSet<Persona> ();
1302       persona_set.add (persona);
1303
1304       this._personas_changed_cb (persona.store, persona_set, persona_set,
1305           null, null, GroupDetails.ChangeReason.NONE);
1306     }
1307
1308   private void _connect_to_persona (Persona persona)
1309     {
1310       foreach (var prop_name in persona.linkable_properties)
1311         {
1312           persona.notify[prop_name].connect (
1313               this._persona_linkable_property_changed_cb);
1314         }
1315
1316       var al = persona as AntiLinkable;
1317       if (al != null)
1318         {
1319           al.notify["anti-links"].connect (this._persona_anti_links_changed_cb);
1320         }
1321     }
1322
1323   private void _disconnect_from_persona (Persona persona)
1324     {
1325       var al = persona as AntiLinkable;
1326       if (al != null)
1327         {
1328           al.notify["anti-links"].disconnect (
1329               this._persona_anti_links_changed_cb);
1330         }
1331
1332       foreach (var prop_name in persona.linkable_properties)
1333         {
1334           persona.notify[prop_name].disconnect (
1335               this._persona_linkable_property_changed_cb);
1336         }
1337     }
1338
1339   private void _add_persona_to_link_map (Persona persona, Individual individual)
1340     {
1341       debug ("Connecting to Persona: %s (is user: %s, IID: %s)", persona.uid,
1342           persona.is_user ? "yes" : "no", persona.iid);
1343       debug ("    Mapping to Individual: %s", individual.id);
1344
1345       /* Add the Persona to the link map. Its trust level will be reflected in
1346        * final_individual.trust_level, so other Personas won't be linked against
1347        * it in error if the trust level is NONE. */
1348       this._link_map.set (persona.iid, individual);
1349
1350       /* Only allow linking on non-IID properties of the Persona if we fully
1351        * trust the PersonaStore it came from. */
1352       if (persona.store.trust_level == PersonaStoreTrust.FULL)
1353         {
1354           debug ("    Inserting links:");
1355
1356           /* Insert maps from the Persona's linkable properties to the
1357            * Individual. */
1358           foreach (unowned string prop_name in persona.linkable_properties)
1359             {
1360               debug ("        %s", prop_name);
1361
1362               /* FIXME: can't be var because of bgo#638208 */
1363               unowned ObjectClass pclass = persona.get_class ();
1364               if (pclass.find_property (prop_name) == null)
1365                 {
1366                   warning (
1367                       /* Translators: the parameter is a property name. */
1368                       _("Unknown property '%s' in linkable property list."),
1369                       prop_name);
1370                   continue;
1371                 }
1372
1373               persona.linkable_property_to_links (prop_name, (l) =>
1374                 {
1375                   unowned string prop_linking_value = l;
1376
1377                   debug ("            %s", prop_linking_value);
1378                   this._link_map.set (prop_linking_value, individual);
1379                 });
1380             }
1381         }
1382     }
1383
1384   /* We remove individuals as a whole from the link map, rather than iterating
1385    * through the link map keys generated by their personas (as in
1386    * _add_persona_to_link_map()) because the values of the personas' linkable
1387    * properties may well have changed since we added the personas to the link
1388    * map. If that's the case, we don't want to end up leaving stale entries in
1389    * the link map, since that *will* cause problems later on. */
1390   private void _remove_individual_from_link_map (Individual individual)
1391     {
1392       debug ("Removing Individual '%s' from the link map.", individual.id);
1393
1394       /* We have to list the keys to remove and then remove them later, since
1395        * we can't modify the link map while we're iterating through it, and
1396        * there's no iterator object. FIXME: bgo#675067 */
1397       var remove_keys = new string[0];
1398
1399       foreach (var link_key in this._link_map.get_keys ())
1400         {
1401           if (this._link_map.get (link_key).contains (individual))
1402             {
1403               debug ("    %s → %s (%p)",
1404                   link_key, individual.id, individual);
1405
1406               remove_keys += link_key;
1407             }
1408         }
1409
1410       /* Actually remove the keys. */
1411       foreach (var link_key in remove_keys)
1412         {
1413           this._link_map.remove (link_key, individual);
1414         }
1415     }
1416
1417   private void _personas_changed_cb (PersonaStore store,
1418       Set<Persona> added,
1419       Set<Persona> removed,
1420       string? message,
1421       Persona? actor,
1422       GroupDetails.ChangeReason reason)
1423     {
1424       var removed_individuals = new HashSet<Individual> ();
1425       var individuals_changes = new HashMultiMap<Individual?, Individual?> ();
1426       var relinked_personas = new HashSet<Persona> ();
1427       var replaced_individuals = new HashMap<Individual, Individual> ();
1428
1429       /* We store the value of this.user locally and only update it at the end
1430        * of the function to prevent spamming notifications of changes to the
1431        * property. */
1432       var user = this.user;
1433
1434       debug ("Removing Personas:");
1435
1436       foreach (var persona in removed)
1437         {
1438           debug ("    %s (is user: %s, IID: %s)", persona.uid,
1439               persona.is_user ? "yes" : "no", persona.iid);
1440
1441           /* Find the Individual containing the Persona (if any) and mark them
1442            * for removal (any other Personas they have which aren't being
1443            * removed will be re-linked into other Individuals). */
1444           Individual? ind = persona.individual;
1445           if (ind != null)
1446             {
1447               removed_individuals.add ((!) ind);
1448             }
1449
1450           /* Stop listening to notifications about the persona's linkable
1451            * properties. */
1452           this._disconnect_from_persona (persona);
1453         }
1454
1455       /* Remove the Individuals which were pointed to by the linkable properties
1456        * of the removed Personas. We can then re-link the other Personas in
1457        * those Individuals, since their links may have changed.
1458        * Note that we remove the Individual from this.individuals, meaning that
1459        * _individual_removed_cb() ignores this Individual. This allows us to
1460        * group together the IndividualAggregator.individuals_changed signals
1461        * for all the removed Individuals. */
1462       debug ("Removing Individuals due to removed links:");
1463       foreach (var individual in removed_individuals)
1464         {
1465           /* Ensure we don't remove the same Individual twice */
1466           if (this._individuals.has_key (individual.id) == false)
1467             continue;
1468
1469           debug ("    %s", individual.id);
1470
1471           /* Build a list of Personas which need relinking. Ensure we don't
1472            * include any of the Personas which have just been removed. */
1473           foreach (var persona in individual.personas)
1474             {
1475               if (removed.contains (persona) == true ||
1476                   relinked_personas.contains (persona) == true)
1477                 continue;
1478
1479               relinked_personas.add (persona);
1480             }
1481
1482           if (user == individual)
1483             user = null;
1484
1485           this._disconnect_from_individual (individual);
1486
1487           /* Remove the Individual's links from the link map */
1488           this._remove_individual_from_link_map (individual);
1489         }
1490
1491       debug ("Adding Personas:");
1492       foreach (var persona in added)
1493         {
1494           debug ("    %s (is user: %s, IID: %s)", persona.uid,
1495               persona.is_user ? "yes" : "no", persona.iid);
1496
1497           /* Connect to notifications about the persona's linkable
1498            * properties. */
1499           this._connect_to_persona (persona);
1500         }
1501
1502       if (added.size > 0)
1503         {
1504           this._add_personas (added, ref user, ref individuals_changes);
1505         }
1506
1507       debug ("Relinking Personas:");
1508       foreach (var persona in relinked_personas)
1509         {
1510           debug ("    %s (is user: %s, IID: %s)", persona.uid,
1511               persona.is_user ? "yes" : "no", persona.iid);
1512         }
1513
1514       this._add_personas (relinked_personas, ref user, ref individuals_changes);
1515
1516       /* Work out which final individuals have replaced the removed_individuals
1517        * and update individuals_changes accordingly. */
1518       foreach (var individual in removed_individuals)
1519         {
1520           var added_mapping = false;
1521
1522           foreach (var persona in individual.personas)
1523             {
1524               if (!(persona in removed) || (persona in added))
1525                 {
1526                   individuals_changes.remove (null, persona.individual);
1527                   individuals_changes.set (individual, persona.individual);
1528                   added_mapping = true;
1529                 }
1530             }
1531
1532           /* Has the individual been removed entirely? */
1533           if (added_mapping == false)
1534             {
1535               individuals_changes.set (individual, null);
1536             }
1537
1538           individual.personas = null;
1539         }
1540
1541       /* Notify of changes to this.user */
1542       this.user = user;
1543
1544       /* Signal the addition of new individuals and removal of old ones to the
1545        * aggregator */
1546       if (individuals_changes.size > 0)
1547         {
1548           var added_individuals = new HashSet<Individual> ();
1549
1550           /* Extract the deprecated added and removed sets from
1551            * individuals_changes, to be used in the individuals_changed
1552            * signal. */
1553           foreach (var old_ind in individuals_changes.get_keys ())
1554             {
1555               foreach (var new_ind in individuals_changes.get (old_ind))
1556                 {
1557                   assert (old_ind != null || new_ind != null);
1558
1559                   if (old_ind != null)
1560                     {
1561                       removed_individuals.add ((!) old_ind);
1562                     }
1563
1564                   if (new_ind != null)
1565                     {
1566                       added_individuals.add ((!) new_ind);
1567                       this._connect_to_individual ((!) new_ind);
1568                     }
1569
1570                   if (old_ind != null && new_ind != null)
1571                     {
1572                       replaced_individuals.set ((!) old_ind, (!) new_ind);
1573                     }
1574                 }
1575             }
1576
1577           this._emit_individuals_changed (added_individuals,
1578               removed_individuals, individuals_changes);
1579         }
1580
1581       /* Signal the replacement of various Individuals as a consequence of
1582        * linking. */
1583       debug ("Replacing Individuals due to linking:");
1584       var iter = replaced_individuals.map_iterator ();
1585       while (iter.next () == true)
1586         {
1587           var old_ind = iter.get_key ();
1588           var new_ind = iter.get_value ();
1589
1590           debug ("    %s (%p) → %s (%p)", old_ind.id, old_ind,
1591               new_ind.id, new_ind);
1592
1593           old_ind.replace (new_ind);
1594         }
1595
1596       /* Validate the link map. */
1597       if (this._debug.debug_output_enabled == true)
1598         {
1599           foreach (var link_key in this._link_map.get_keys ())
1600             {
1601               foreach (var individual in this._link_map.get (link_key))
1602                 {
1603                   if (this._individuals.get (individual.id) != individual)
1604                     {
1605                       warning ("Link map contains invalid mapping:\n" +
1606                           "    %s → %s (%p)",
1607                               link_key, individual.id, individual);
1608                       warning ("Individual %s (%p) personas:", individual.id,
1609                           individual);
1610                       foreach (var p in individual.personas)
1611                         {
1612                           warning ("    %s (%p)", p.uid, p);
1613                         }
1614                     }
1615                 }
1616             }
1617         }
1618     }
1619
1620   private void _is_primary_store_changed_cb (Object object, ParamSpec pspec)
1621     {
1622       /* Ensure that we only have one primary PersonaStore */
1623       var store = (PersonaStore) object;
1624       assert ((store.is_primary_store == true &&
1625               store == this._primary_store) ||
1626           (store.is_primary_store == false &&
1627               store != this._primary_store));
1628     }
1629
1630   private void _persona_store_is_quiescent_changed_cb (Object obj,
1631       ParamSpec pspec)
1632     {
1633       /* Have we reached a quiescent state yet? */
1634       if (this._non_quiescent_persona_store_count > 0)
1635         {
1636           this._non_quiescent_persona_store_count--;
1637           this._notify_if_is_quiescent ();
1638         }
1639     }
1640
1641   private void _backend_is_quiescent_changed_cb (Object obj, ParamSpec pspec)
1642     {
1643       if (this._non_quiescent_backend_count > 0)
1644         {
1645           this._non_quiescent_backend_count--;
1646           this._notify_if_is_quiescent ();
1647         }
1648     }
1649
1650   private void _notify_if_is_quiescent ()
1651     {
1652       if (this._non_quiescent_backend_count == 0 &&
1653           this._non_quiescent_persona_store_count == 0 &&
1654           this._is_quiescent == false)
1655         {
1656           if (this._configured_primary_store_type_id.length > 0 &&
1657               this._primary_store == null)
1658             {
1659               warning ("Failed to find primary PersonaStore with type ID " +
1660                   "'%s' and ID '%s'.\n" +
1661                   "Individuals will not be linked properly " +
1662                   "and creating new links between Personas will not work.\n" +
1663                   "The configured primary PersonaStore's backend may not be " +
1664                   "installed. If you are unsure, check with your " +
1665                   "distribution.",
1666                   this._configured_primary_store_type_id,
1667                   this._configured_primary_store_id);
1668             }
1669
1670           Internal.profiling_point ("reached quiescence in " +
1671               "IndividualAggregator");
1672
1673           this._is_quiescent = true;
1674           this.notify_property ("is-quiescent");
1675
1676           /* Remove the quiescence timeout, if it exists. */
1677           if (this._quiescent_timeout_id != 0)
1678             {
1679               Source.remove (this._quiescent_timeout_id);
1680               this._quiescent_timeout_id = 0;
1681             }
1682         }
1683     }
1684
1685   private bool _quiescent_timeout_cb ()
1686     {
1687       /* If we're not quiescent by the time the timeout is triggered, force
1688        * quiescence anyway, just so that we don't leave clients hanging if our
1689        * backends have bugs. */
1690       if (this._is_quiescent == false)
1691         {
1692           warning ("Failed to reach quiescence normally (%u backends and %u " +
1693               "persona stores still haven't reached quiescence). Forcing " +
1694               "IndividualAggregator quiescence due to reaching the timeout.",
1695               this._non_quiescent_backend_count,
1696               this._non_quiescent_persona_store_count);
1697
1698           this._is_quiescent = true;
1699           this.notify_property ("is-quiescent");
1700         }
1701
1702       /* One-shot timeout */
1703       this._quiescent_timeout_id = 0;
1704       return false;
1705     }
1706
1707   private void _persona_store_is_user_set_default_changed_cb (Object obj,
1708       ParamSpec pspec)
1709     {
1710       var store = (PersonaStore) obj;
1711
1712       debug ("PersonaStore.is-user-set-default changed for store %p " +
1713           "(type ID: %s, ID: %s)", store, store.type_id, store.id);
1714
1715       if (this._maybe_configure_as_primary (store))
1716         this._set_primary_store (store);
1717     }
1718
1719   private bool _maybe_configure_as_primary (PersonaStore store)
1720     {
1721       debug ("_maybe_configure_as_primary()");
1722
1723       var configured = false;
1724
1725       if (!this._user_configured_primary_store &&
1726           store.is_user_set_default)
1727         {
1728           debug ("Setting primary store IDs to '%s' and '%s'.", store.type_id,
1729               store.id);
1730           this._configured_primary_store_type_id = store.type_id;
1731           this._configured_primary_store_id = store.id;
1732           configured = true;
1733         }
1734
1735       return configured;
1736     }
1737
1738   private void _individual_removed_cb (Individual i, Individual? replacement)
1739     {
1740       if (this.user == i)
1741         this.user = null;
1742
1743       /* Only signal if the individual is still in this.individuals. This allows
1744        * us to group removals together in, e.g., _personas_changed_cb(). */
1745       if (this._individuals.get (i.id) != i)
1746         return;
1747
1748       if (replacement != null)
1749         {
1750           debug ("Individual '%s' removed (replaced by '%s')", i.id,
1751               ((!) replacement).id);
1752         }
1753       else
1754         {
1755           debug ("Individual '%s' removed (not replaced)", i.id);
1756         }
1757
1758       /* If the individual has 0 personas, we've already signaled removal */
1759       if (i.personas.size > 0)
1760         {
1761           var changes = new HashMultiMap<Individual?, Individual?> ();
1762           var individuals = new HashSet<Individual> ();
1763
1764           individuals.add (i);
1765           changes.set (i, replacement);
1766
1767           this._emit_individuals_changed (null, individuals, changes);
1768         }
1769
1770       this._disconnect_from_individual (i);
1771     }
1772
1773   /**
1774    * Add a new persona in the given {@link PersonaStore} based on the
1775    * ``details`` provided.
1776    *
1777    * If the target store is offline, this function will throw
1778    * {@link IndividualAggregatorError.STORE_OFFLINE}. It's the responsibility of
1779    * the caller to cache details and re-try this function if it wishes to make
1780    * offline adds work.
1781    *
1782    * The details hash is a backend-specific mapping of key, value strings.
1783    * Common keys include:
1784    *
1785    *  * contact - service-specific contact ID
1786    *  * message - a user-readable message to pass to the persona being added
1787    *
1788    * If a {@link Persona} with the given details already exists in the store, no
1789    * error will be thrown and this function will return ``null``.
1790    *
1791    * @param parent an optional {@link Individual} to add the new {@link Persona}
1792    * to. This persona will be appended to its ordered list of personas.
1793    * @param persona_store the {@link PersonaStore} to add the persona to
1794    * @param details a key-value map of details to use in creating the new
1795    * {@link Persona}
1796    * @return the new {@link Persona} or ``null`` if the corresponding
1797    * {@link Persona} already existed. If non-``null``, the new {@link Persona}
1798    * will also be added to a new or existing {@link Individual} as necessary.
1799    * @throws IndividualAggregatorError.STORE_OFFLINE if the persona store was
1800    * offline
1801    * @throws IndividualAggregatorError.ADD_FAILED if any other error occurred
1802    * while adding the persona
1803    *
1804    * @since 0.3.5
1805    */
1806   public async Persona? add_persona_from_details (Individual? parent,
1807       PersonaStore persona_store,
1808       HashTable<string, Value?> details) throws IndividualAggregatorError
1809     {
1810       Persona? persona = null;
1811       try
1812         {
1813           var details_copy = this._asv_copy (details);
1814           persona = yield persona_store.add_persona_from_details (details_copy);
1815         }
1816       catch (PersonaStoreError e)
1817         {
1818           if (e is PersonaStoreError.STORE_OFFLINE)
1819             {
1820               throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
1821             }
1822           else
1823             {
1824               var full_id = this._get_store_full_id (persona_store.type_id,
1825                   persona_store.id);
1826
1827               throw new IndividualAggregatorError.ADD_FAILED (
1828                   /* Translators: the first parameter is a store identifier
1829                    * and the second parameter is an error message. */
1830                   _("Failed to add contact for persona store ID '%s': %s"),
1831                   full_id, e.message);
1832             }
1833         }
1834
1835       if (parent != null && persona != null)
1836         {
1837           ((!) parent).personas.add ((!) persona);
1838         }
1839
1840       return persona;
1841     }
1842
1843   private HashTable<string, Value?> _asv_copy (HashTable<string, Value?> asv)
1844     {
1845       var retval = new HashTable<string, Value?> (str_hash, str_equal);
1846
1847       asv.foreach ((k, v) =>
1848         {
1849           retval.insert ((string) k, v);
1850         });
1851
1852       return retval;
1853     }
1854
1855   /**
1856    * Completely remove the individual and all of its personas from their
1857    * backing stores.
1858    *
1859    * This method is safe to call multiple times concurrently (for the same
1860    * individual or different individuals).
1861    *
1862    * @param individual the {@link Individual} to remove
1863    * @throws GLib.Error if removing the persona failed — this will be passed
1864    * through from {@link PersonaStore.remove_persona}
1865    *
1866    * @since 0.1.11
1867    */
1868   public async void remove_individual (Individual individual) throws GLib.Error
1869     {
1870       /* Removing personas changes the persona set so we need to make a copy
1871        * first */
1872       var personas = new HashSet<Persona> ();
1873       foreach (var p in individual.personas)
1874         {
1875           personas.add (p);
1876         }
1877
1878       foreach (var persona in personas)
1879         {
1880           yield persona.store.remove_persona (persona);
1881         }
1882     }
1883
1884   /**
1885    * Completely remove the persona from its backing store.
1886    *
1887    * This will leave other personas in the same individual alone.
1888    *
1889    * This method is safe to call multiple times concurrently (for the same
1890    * persona or different personas).
1891    *
1892    * @param persona the {@link Persona} to remove
1893    * @throws GLib.Error if removing the persona failed — this will be passed
1894    * through from {@link PersonaStore.remove_persona}
1895    *
1896    * @since 0.1.11
1897    */
1898   public async void remove_persona (Persona persona) throws GLib.Error
1899     {
1900       yield persona.store.remove_persona (persona);
1901     }
1902
1903   /**
1904    * Link the given {@link Persona}s together.
1905    *
1906    * Create links between the given {@link Persona}s so that they form a single
1907    * {@link Individual}. The new {@link Individual} will be returned via the
1908    * {@link IndividualAggregator.individuals_changed} signal.
1909    *
1910    * Removal of the {@link Individual}s which the {@link Persona}s were in
1911    * before is signalled by {@link IndividualAggregator.individuals_changed} and
1912    * {@link Individual.removed}.
1913    *
1914    * This method is safe to call multiple times concurrently.
1915    *
1916    * @param personas the {@link Persona}s to be linked
1917    * @throws IndividualAggregatorError.NO_PRIMARY_STORE if no primary store has
1918    * been configured for the individual aggregator
1919    * @throws IndividualAggregatorError if adding the linking persona failed —
1920    * this will be passed through from
1921    * {@link IndividualAggregator.add_persona_from_details}
1922    *
1923    * @since 0.5.1
1924    */
1925   public async void link_personas (Set<Persona> personas)
1926       throws IndividualAggregatorError
1927     {
1928       if (this._primary_store == null)
1929         {
1930           throw new IndividualAggregatorError.NO_PRIMARY_STORE (
1931               _("Can’t link personas with no primary store.") + "\n" +
1932               _("Persona store ‘%s:%s’ is configured as primary, but could not be found or failed to load.") + "\n" +
1933               _("Check the relevant service is running, or change the default store in that service or using the “%s” GSettings key."),
1934               this._configured_primary_store_type_id,
1935               this._configured_primary_store_id,
1936               "%s %s".printf (IndividualAggregator._FOLKS_GSETTINGS_SCHEMA,
1937                   IndividualAggregator._PRIMARY_STORE_CONFIG_KEY));
1938         }
1939
1940       /* Don't bother linking if it's just one Persona */
1941       if (personas.size <= 1)
1942         return;
1943
1944       /* Disallow linking if it's disabled */
1945       if (this._linking_enabled == false)
1946         {
1947           debug ("Can't link Personas: linking disabled.");
1948           return;
1949         }
1950
1951       /* Remove all edges in the connected graph between the personas from the
1952        * anti-link map to ensure that linking the personas actually succeeds. */
1953       foreach (var p in personas)
1954         {
1955           var al = p as AntiLinkable;
1956           if (al != null)
1957             {
1958               try
1959                 {
1960                   yield ((!) al).remove_anti_links (personas);
1961                 }
1962               catch (PropertyError e)
1963                 {
1964                   throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
1965                       _("Anti-links can't be removed between personas being linked."));
1966                 }
1967             }
1968         }
1969
1970       /* Create a new persona in the primary store which links together the
1971        * given personas */
1972       assert (((!) this._primary_store).type_id ==
1973           this._configured_primary_store_type_id);
1974
1975       var details = this._build_linking_details (personas);
1976
1977       yield this.add_persona_from_details (null,
1978           (!) this._primary_store, details);
1979     }
1980
1981   private HashTable<string, Value?> _build_linking_details (
1982       Set<Persona> personas)
1983     {
1984       /* ``protocols_addrs_set`` will be passed to the new Kf.Persona */
1985       var protocols_addrs_set = new HashMultiMap<string, ImFieldDetails> (
1986             null, null, AbstractFieldDetails<string>.hash_static,
1987             AbstractFieldDetails<string>.equal_static);
1988       var web_service_addrs_set =
1989         new HashMultiMap<string, WebServiceFieldDetails> (
1990             null, null, AbstractFieldDetails<string>.hash_static,
1991             AbstractFieldDetails<string>.equal_static);
1992
1993       /* List of local_ids */
1994       var local_ids = new Gee.HashSet<string> ();
1995
1996       foreach (var persona in personas)
1997         {
1998           if (persona is ImDetails)
1999             {
2000               ImDetails im_details = (ImDetails) persona;
2001
2002               /* protocols_addrs_set = union (all personas' IM addresses) */
2003               foreach (var protocol in im_details.im_addresses.get_keys ())
2004                 {
2005                   var im_addresses = im_details.im_addresses.get (protocol);
2006
2007                   foreach (var im_address in im_addresses)
2008                     {
2009                       protocols_addrs_set.set (protocol, im_address);
2010                     }
2011                 }
2012             }
2013
2014           if (persona is WebServiceDetails)
2015             {
2016               WebServiceDetails ws_details = (WebServiceDetails) persona;
2017
2018               /* web_service_addrs_set = union (all personas' WS addresses) */
2019               foreach (var web_service in
2020                   ws_details.web_service_addresses.get_keys ())
2021                 {
2022                   var ws_addresses =
2023                       ws_details.web_service_addresses.get (web_service);
2024
2025                   foreach (var ws_fd in ws_addresses)
2026                     web_service_addrs_set.set (web_service, ws_fd);
2027                 }
2028             }
2029
2030           if (persona is LocalIdDetails)
2031             {
2032               foreach (var id in ((LocalIdDetails) persona).local_ids)
2033                 {
2034                   local_ids.add (id);
2035                 }
2036             }
2037         }
2038
2039       var details = new HashTable<string, Value?> (str_hash, str_equal);
2040
2041       if (protocols_addrs_set.size > 0)
2042         {
2043           var im_addresses_value = Value (typeof (MultiMap));
2044           im_addresses_value.set_object (protocols_addrs_set);
2045           details.insert (
2046               (!) PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES),
2047               im_addresses_value);
2048         }
2049
2050       if (web_service_addrs_set.size > 0)
2051         {
2052           var web_service_addresses_value = Value (typeof (MultiMap));
2053           web_service_addresses_value.set_object (web_service_addrs_set);
2054           details.insert (
2055               (!) PersonaStore.detail_key (PersonaDetail.WEB_SERVICE_ADDRESSES),
2056               web_service_addresses_value);
2057         }
2058
2059       if (local_ids.size > 0)
2060         {
2061           var local_ids_value = Value (typeof (Set));
2062           local_ids_value.set_object (local_ids);
2063           details.insert (
2064               (!) Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
2065               local_ids_value);
2066         }
2067
2068       return details;
2069     }
2070
2071   /**
2072    * Unlinks the given {@link Individual} into its constituent {@link Persona}s.
2073    *
2074    * This completely unlinks the given {@link Individual}, destroying all of
2075    * its writeable {@link Persona}s.
2076    *
2077    * The {@link Individual}'s removal is signalled by
2078    * {@link IndividualAggregator.individuals_changed} and
2079    * {@link Individual.removed}.
2080    *
2081    * The {@link Persona}s comprising the {@link Individual} will be re-linked
2082    * into one or more new {@link Individual}s, depending on how much linking
2083    * data remains (typically only implicit links remain). The addition of these
2084    * new {@link Individual}s will be signalled by
2085    * {@link IndividualAggregator.individuals_changed}.
2086    *
2087    * This method is safe to call multiple times concurrently, although
2088    * concurrent calls for the same individual may result in duplicate personas
2089    * being created.
2090    *
2091    * @param individual the {@link Individual} to unlink
2092    * @throws GLib.Error if removing the linking persona failed — this will be
2093    * passed through from {@link PersonaStore.remove_persona}
2094    *
2095    * @since 0.1.13
2096    */
2097   public async void unlink_individual (Individual individual) throws GLib.Error
2098     {
2099       if (this._linking_enabled == false)
2100         {
2101           debug ("Can't unlink Individual '%s': linking disabled.",
2102               individual.id);
2103           return;
2104         }
2105
2106       debug ("Unlinking Individual '%s':", individual.id);
2107
2108       /* Add all edges in the connected graph between the personas to the
2109        * anti-link map to ensure that unlinking the personas actually succeeds,
2110        * and that they aren't immediately re-linked.
2111        *
2112        * Perversely, this requires that we ensure the anti-links property is
2113        * writeable on all personas before continuing. Ignore errors from it in
2114        * the hope that everything works anyway.
2115        *
2116        * In the worst case, this will double the number of personas, since if
2117        * none of the personas have anti-links writeable, each will have to be
2118        * linked with a new writeable persona. */
2119       var individual_personas = new HashSet<Persona> (); /* as we modify it */
2120       individual_personas.add_all (individual.personas);
2121
2122       debug ("    Inserting anti-links:");
2123       foreach (var pers in individual_personas)
2124         {
2125           try
2126             {
2127               var personas = new HashSet<Persona> ();
2128               personas.add (pers);
2129               debug ("        Anti-linking persona '%s' (%p)", pers.uid, pers);
2130
2131               var writeable_persona =
2132                   yield this._ensure_personas_property_writeable (personas,
2133                       "anti-links");
2134               debug ("        Writeable persona '%s' (%p)",
2135                   writeable_persona.uid, writeable_persona);
2136
2137               /* Make sure not to anti-link the new persona to pers. */
2138               var anti_link_personas = new HashSet<Persona> ();
2139               anti_link_personas.add_all (individual_personas);
2140               anti_link_personas.remove (pers);
2141
2142               var al = writeable_persona as AntiLinkable;
2143               assert (al != null);
2144               yield ((!) al).add_anti_links (anti_link_personas);
2145               debug ("");
2146             }
2147           catch (IndividualAggregatorError e1)
2148             {
2149               debug ("    Failed to ensure anti-links property is writeable " +
2150                   "(continuing anyway): %s", e1.message);
2151             }
2152         }
2153     }
2154
2155   /**
2156    * Ensure that the given property is writeable for the given
2157    * {@link Individual}.
2158    *
2159    * This makes sure that there is at least one {@link Persona} in the
2160    * individual which has ``property_name`` in its
2161    * {@link Persona.writeable_properties}. If no such persona exists in the
2162    * individual, a new one will be created and linked to the individual. (Note
2163    * that due to the design of the aggregator, this will result in the previous
2164    * individual being removed and replaced by a new one with the new persona;
2165    * listen to the {@link Individual.removed} signal to see the replacement.)
2166    *
2167    * It may not be possible to create a new persona which has the given property
2168    * as writeable. In that case, a
2169    * {@link IndividualAggregatorError.NO_PRIMARY_STORE} or
2170    * {@link IndividualAggregatorError.PROPERTY_NOT_WRITEABLE} error will be
2171    * thrown.
2172    *
2173    * This method is safe to call multiple times concurrently, although
2174    * concurrent calls for the same individual may result in duplicate personas
2175    * being created.
2176    *
2177    * @param individual the individual for which ``property_name`` should be
2178    * writeable
2179    * @param property_name the name of the property which needs to be writeable
2180    * (this should be in lower case using hyphens, e.g. “web-service-addresses”)
2181    * @return a persona (new or existing) which has the given property as
2182    * writeable
2183    * @throws IndividualAggregatorError.NO_PRIMARY_STORE if no primary store was
2184    * configured for this individual aggregator
2185    * @throws IndividualAggregatorError.PROPERTY_NOT_WRITEABLE if the given
2186    * ``property_name`` referred to a non-writeable property
2187    * @throws IndividualAggregatorError if adding a new persona (using
2188    * {@link IndividualAggregator.add_persona_from_details}) failed, or if
2189    * linking personas (using {@link IndividualAggregator.link_personas}) failed
2190    *
2191    * @since 0.6.2
2192    */
2193   public async Persona ensure_individual_property_writeable (
2194       Individual individual, string property_name)
2195       throws IndividualAggregatorError
2196     {
2197       debug ("ensure_individual_property_writeable: %s, %s",
2198           individual.id, property_name);
2199
2200       var p = yield this._ensure_personas_property_writeable (
2201           individual.personas, property_name);
2202       return p;
2203     }
2204
2205   /* This is safe to call multiple times concurrently, *but* if the set of
2206    * personas doesn't change, multiple duplicate personas may be created in the
2207    * writeable store. */
2208   private async Persona _ensure_personas_property_writeable (
2209       Set<Persona> personas, string property_name)
2210       throws IndividualAggregatorError
2211     {
2212       /* See if the persona set already contains the property we want. */
2213       foreach (var p1 in personas)
2214         {
2215           if (property_name in p1.writeable_properties)
2216             {
2217               debug ("    Returning existing persona: %s", p1.uid);
2218               return p1;
2219             }
2220         }
2221
2222       /* Otherwise, create a new persona in the writeable store. If the
2223        * writeable store doesn't exist or doesn't support writing to the given
2224        * property, we try the other persona stores. */
2225       var details = this._build_linking_details (personas);
2226       Persona? new_persona = null;
2227
2228       if (this._primary_store != null &&
2229           property_name in
2230               ((!) this._primary_store).always_writeable_properties)
2231         {
2232           try
2233             {
2234               debug ("    Using writeable store");
2235               new_persona = yield this.add_persona_from_details (null,
2236                   (!) this._primary_store, details);
2237             }
2238           catch (IndividualAggregatorError e1)
2239             {
2240               /* Ignore it */
2241               new_persona = null;
2242             }
2243         }
2244
2245       if (new_persona == null)
2246         {
2247           foreach (var s in this._stores.values)
2248             {
2249               if (s == this._primary_store ||
2250                   !(property_name in s.always_writeable_properties))
2251                 {
2252                   /* Skip the store we've just tried */
2253                   continue;
2254                 }
2255
2256               try
2257                 {
2258                   debug ("    Using store %s", s.id);
2259                   new_persona = yield this.add_persona_from_details (null, s,
2260                       details);
2261                 }
2262               catch (IndividualAggregatorError e2)
2263                 {
2264                   /* Ignore it */
2265                   new_persona = null;
2266                   continue;
2267                 }
2268             }
2269         }
2270
2271       /* Throw an error if we haven't managed to find a suitable store */
2272       if (new_persona == null && this._primary_store == null)
2273         {
2274           throw new IndividualAggregatorError.NO_PRIMARY_STORE (
2275               _("Can’t add personas with no primary store.") + "\n" +
2276               _("Persona store ‘%s:%s’ is configured as primary, but could not be found or failed to load.") + "\n" +
2277               _("Check the relevant service is running, or change the default store in that service or using the “%s” GSettings key."),
2278               this._configured_primary_store_type_id,
2279               this._configured_primary_store_id,
2280               "%s %s".printf (IndividualAggregator._FOLKS_GSETTINGS_SCHEMA,
2281                   IndividualAggregator._PRIMARY_STORE_CONFIG_KEY));
2282         }
2283       else if (new_persona == null)
2284         {
2285           throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
2286               _("Can't write to requested property (“%s”) of the writeable store."),
2287               property_name);
2288         }
2289
2290       /* We can guarantee new_persona != null because we'd have bailed out above
2291        * otherwise. */
2292       return (!) new_persona;
2293     }
2294
2295   /**
2296    * Look up an individual in the aggregator.
2297    *
2298    * This returns the {@link Individual} with the given ``id`` if it exists in
2299    * the aggregator, and ``null`` otherwise.
2300    *
2301    * In future, when lazy-loading of individuals' properties is added to folks,
2302    * this method guarantees to load all properties of the individual, even if
2303    * the aggregator hasn't lazy-loaded anything else.
2304    *
2305    * This method is safe to call before {@link IndividualAggregator.prepare} has
2306    * been called, and will call {@link IndividualAggregator.prepare} itself in
2307    * that case.
2308    *
2309    * This method is safe to call multiple times concurrently.
2310    *
2311    * @param id ID of the individual to look up
2312    * @return individual with ``id``, or ``null`` if no such individual was found
2313    * @throws GLib.Error from {@link IndividualAggregator.prepare}
2314    *
2315    * @since 0.7.0
2316    */
2317   public async Individual? look_up_individual (string id) throws GLib.Error
2318     {
2319       /* Ensure the aggregator's prepared. */
2320       yield this.prepare ();
2321
2322       /* FIXME: When bgo#648805 is fixed, this needs to support lazy-loading. */
2323       return this._individuals.get (id);
2324     }
2325 }