2 * Copyright (C) 2010 Collabora Ltd.
3 * Copyright (C) 2012, 2013 Philip Withnall
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.
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.
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/>.
19 * Travis Reitter <travis.reitter@collabora.co.uk>
20 * Philip Withnall <philip@tecnocode.co.uk>
27 * Errors from {@link IndividualAggregator}s.
29 public errordomain Folks.IndividualAggregatorError
32 * Adding a {@link Persona} to a {@link PersonaStore} failed.
37 * An operation which required the use of a writeable store failed because no
38 * writeable store was available.
42 [Deprecated (since = "0.6.2.1",
43 replacement = "IndividualAggregatorError.NO_PRIMARY_STORE")]
47 * The {@link PersonaStore} was offline (ie, this is a temporary failure).
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.
60 PROPERTY_NOT_WRITEABLE,
63 * An operation which required the use of a primary store failed because no
64 * primary store was available.
72 * Stores {@link Individual}s which have been created through
73 * aggregation of all the {@link Persona}s provided by the various
76 * This is the main interface for client applications.
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}:
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);
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
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
106 * An existing individual may be unlinked to form singleton
107 * individuals for each of its personas:
109 * yield my_individual_aggregator.unlink_individual (my_individual);
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.
117 public class Folks.IndividualAggregator : Object
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";
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;
149 private static const uint _QUIESCENT_TIMEOUT = 30; /* seconds */
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;
158 * Whether {@link IndividualAggregator.prepare} has successfully completed for
163 public bool is_prepared
165 get { return this._is_prepared; }
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``).
176 * It's guaranteed that this property's value will only ever change after
177 * {@link IndividualAggregator.is_prepared} has changed to ``true``.
181 public bool is_quiescent
183 get { return this._is_quiescent; }
187 * Our configured primary (writeable) store.
189 * Which one to use is decided (in order or precedence)
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
198 public PersonaStore? primary_store
200 get { return this._primary_store; }
203 private Map<string, Individual> _individuals;
204 private Map<string, Individual> _individuals_ro;
207 * A map from {@link Individual.id}s to their {@link Individual}s.
209 * This is the canonical set of {@link Individual}s provided by this
210 * IndividualAggregator.
212 * {@link Individual}s may be added or removed using
213 * {@link IndividualAggregator.add_persona_from_details} and
214 * {@link IndividualAggregator.remove_individual}, respectively.
218 public Map<string, Individual> individuals
220 get { return this._individuals_ro; }
223 this._individuals = value;
224 this._individuals_ro = this._individuals.read_only_view;
229 * The {@link Individual} representing the user.
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.
237 public Individual? user { get; private set; }
240 * Emitted when one or more {@link Individual}s are added to or removed from
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.
248 * This will not be emitted until after {@link IndividualAggregator.prepare}
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
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,
265 GroupDetails.ChangeReason reason);
268 * Emitted when one or more {@link Individual}s are added to or removed from
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.
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
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
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.
291 * Individuals which have been removed will be listed in the multi-map as
292 * mappings from the removed individual to ``null``.
294 * This will not be emitted until after {@link IndividualAggregator.prepare}
297 * @param changes a mapping of old {@link Individual}s to new
298 * {@link Individual}s for the individuals which have changed in the
303 public signal void individuals_changed_detailed (
304 MultiMap<Individual?, Individual?> changes);
306 /* FIXME: make this a singleton? */
308 * Create a new IndividualAggregator.
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.
316 * An example of how to set up an IndividualAggregator:
318 * IndividualAggregator agg = new IndividualAggregator ();
319 * agg.individuals_changed_detailed.connect (individuals_changed_cb);
323 public IndividualAggregator ()
326 this._backend_store = BackendStore.dup ();
330 * Create a new IndividualAggregator with a custom {@link BackendStore}.
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.
336 * @param store the {@link BackendStore} to use instead of the default one.
340 public IndividualAggregator.with_backend_store (BackendStore store)
343 this._backend_store = store;
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> ();
353 this._backends = new HashSet<Backend> ();
354 this._debug = Debug.dup ();
355 this._debug.print_status.connect (this._debug_print_status);
357 /* Check out the configured primary store */
358 var store_config_ids = Environment.get_variable ("FOLKS_PRIMARY_STORE");
359 if (store_config_ids == null)
361 store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
362 if (store_config_ids != null)
364 var deprecated_warn = "FOLKS_WRITEABLE_STORE is deprecated, ";
365 deprecated_warn += "use FOLKS_PRIMARY_STORE";
366 warning (deprecated_warn);
370 if (store_config_ids != null)
372 debug ("Setting primary store IDs from environment variable.");
373 this._configure_primary_store ((!) store_config_ids);
377 debug ("Setting primary store IDs to defaults.");
378 if (BuildConf.HAVE_EDS)
380 this._configured_primary_store_type_id = "eds";
381 this._configured_primary_store_id = "system-address-book";
385 this._configured_primary_store_type_id = "key-file";
386 this._configured_primary_store_id = "";
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 != "")
393 debug ("Setting primary store IDs from GSettings.");
394 this._configure_primary_store ((!) val);
398 debug ("Primary store IDs are '%s' and '%s'.",
399 this._configured_primary_store_type_id,
400 this._configured_primary_store_id);
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");
408 this._backend_store = BackendStore.dup ();
410 debug ("Constructing IndividualAggregator %p", this);
413 ~IndividualAggregator ()
415 debug ("Destroying IndividualAggregator %p", this);
417 if (this._quiescent_timeout_id != 0)
419 Source.remove (this._quiescent_timeout_id);
420 this._quiescent_timeout_id = 0;
423 this._backend_store.backend_available.disconnect (
424 this._backend_available_cb);
426 this._debug.print_status.disconnect (this._debug_print_status);
429 private void _configure_primary_store (string store_config_ids)
431 debug ("_configure_primary_store to '%s'", store_config_ids);
432 this._user_configured_primary_store = true;
434 if (store_config_ids.index_of (":") != -1)
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];
442 this._configured_primary_store_type_id = store_config_ids;
443 this._configured_primary_store_id = "";
447 private void _debug_print_status (Debug debug)
449 const string domain = Debug.STATUS_LOG_DOMAIN;
450 const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
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
462 : "no (%u backends, %u persona stores left)".printf (
463 this._non_quiescent_backend_count,
464 this._non_quiescent_persona_store_count)
467 debug.print_line (domain, level,
468 "%u Individuals:", this.individuals.size);
471 foreach (var individual in this.individuals.values)
473 string? trust_level = null;
475 switch (individual.trust_level)
477 case TrustLevel.NONE:
478 trust_level = "none";
480 case TrustLevel.PERSONAS:
481 trust_level = "personas";
484 assert_not_reached ();
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 (),
491 "User?", individual.is_user ? "yes" : "no",
492 "Trust level", trust_level
494 debug.print_line (domain, level, "%u Personas:",
495 individual.personas.size);
499 foreach (var persona in individual.personas)
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 (),
506 "Display ID", persona.display_id,
507 "User?", persona.is_user ? "yes" : "no"
516 debug.print_line (domain, level, "%u entries in the link map:",
517 this._link_map.size);
520 foreach (var link_key in this._link_map.get_keys ())
522 debug.print_line (domain, level, "%s → {", link_key);
525 foreach (var individual in this._link_map.get (link_key))
527 debug.print_line (domain, level, "%p", individual);
531 debug.print_line (domain, level, "}");
536 debug.print_line (domain, level, "");
540 * Prepare the IndividualAggregator for use.
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.
549 * This function is guaranteed to be idempotent (since version 0.3.0).
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
557 * @throws GLib.Error if preparing any of the backends failed — this error
558 * will be passed through from {@link BackendStore.load_backends}
562 public async void prepare () throws GLib.Error
564 Internal.profiling_start ("preparing IndividualAggregator");
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. */
570 if (this._is_prepared || this._prepare_pending)
577 this._prepare_pending = true;
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++;
584 this._backend_store.backend_available.connect (
585 this._backend_available_cb);
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)
593 this._backend_available_cb (this._backend_store, backend);
596 /* Load any backends which haven't been loaded already. (Typically
598 yield this._backend_store.load_backends ();
600 this._non_quiescent_backend_count--;
602 this._is_prepared = true;
603 this._prepare_pending = false;
604 this.notify_property ("is-prepared");
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)
612 this._notify_if_is_quiescent ();
617 this._prepare_pending = false;
620 Internal.profiling_end ("preparing IndividualAggregator");
624 * Clean up and release resources used by the aggregator.
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.
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
639 * @throws GLib.Error if unpreparing the backend-specific services failed —
640 * this will be a backend-specific error
642 public async void unprepare () throws GLib.Error
644 lock (this._is_prepared)
646 if (!this._is_prepared || this._prepare_pending)
653 /* Flush any PersonaStores which need it. */
654 foreach (var p in this._stores.values)
661 this._prepare_pending = false;
667 * Get all matches for a given {@link Individual}.
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
677 public Map<Individual, MatchResult> get_potential_matches
678 (Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
680 HashMap<Individual, MatchResult> matches =
681 new HashMap<Individual, MatchResult> ();
682 Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
684 foreach (var i in this._individuals.values)
686 if (i.id == matchee.id)
689 var result = matchObj.potential_match (i, matchee);
690 if (result >= min_threshold)
692 matches.set (i, result);
700 * Get all combinations between all {@link Individual}s.
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``)
710 public Map<Individual, Map<Individual, MatchResult>>
711 get_all_potential_matches
712 (MatchResult min_threshold = MatchResult.VERY_HIGH)
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 ();
719 for (var i = 0; i < individuals.length; i++)
721 var a = individuals[i];
723 HashMap<Individual, MatchResult>? _matches_a = matches.get (a);
724 HashMap<Individual, MatchResult> matches_a;
725 if (_matches_a == null)
727 matches_a = new HashMap<Individual, MatchResult> ();
728 matches.set (a, matches_a);
732 matches_a = (!) _matches_a;
735 for (var f = i + 1; f < individuals.length; f++)
737 var b = individuals[f];
739 HashMap<Individual, MatchResult>? _matches_b = matches.get (b);
740 HashMap<Individual, MatchResult> matches_b;
741 if (_matches_b == null)
743 matches_b = new HashMap<Individual, MatchResult> ();
744 matches.set (b, matches_b);
748 matches_b = (!) _matches_b;
751 var result = matchObj.potential_match (a, b);
753 if (result >= min_threshold)
755 matches_a.set (b, result);
756 matches_b.set (a, result);
764 private async void _add_backend (Backend backend)
766 if (!this._backends.contains (backend))
768 this._backends.add (backend);
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);
777 /* Handle the stores that have already been signaled. Since
778 * this might change while we are looping, get a copy first.
780 var stores = backend.persona_stores.values.to_array ();
781 foreach (var persona_store in stores)
783 this._backend_persona_store_added_cb (backend, persona_store);
788 private void _backend_available_cb (BackendStore backend_store,
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
796 if (backend.is_quiescent == false)
798 this._non_quiescent_backend_count++;
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)
804 this._quiescent_timeout_id =
805 Timeout.add_seconds (IndividualAggregator._QUIESCENT_TIMEOUT,
806 this._quiescent_timeout_cb);
810 this._add_backend.begin (backend);
813 private void _set_primary_store (PersonaStore store)
815 debug ("_set_primary_store()");
817 if (this._primary_store == store)
820 /* We use the configured PersonaStore as the primary PersonaStore.
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.
826 if (store.type_id == this._configured_primary_store_type_id)
828 if ((store.type_id != "eds" &&
829 this._configured_primary_store_id == "") ||
830 this._configured_primary_store_id == store.id)
832 debug ("Setting primary store to %p (type ID: %s, ID: %s)",
833 store, store.type_id, store.id);
835 var previous_store = this._primary_store;
836 this._primary_store = store;
838 store.freeze_notify ();
839 if (previous_store != null)
841 ((!) previous_store).freeze_notify ();
842 ((!) previous_store).is_primary_store = false;
844 store.is_primary_store = true;
845 if (previous_store != null)
846 ((!) previous_store).thaw_notify ();
847 store.thaw_notify ();
849 this.notify_property ("primary-store");
854 private void _backend_persona_store_added_cb (Backend backend,
857 debug ("_backend_persona_store_added_cb(): backend: %s, store: %s (%p)",
858 backend.name, store.id, store);
860 var store_id = this._get_store_full_id (store.type_id, store.id);
862 this._maybe_configure_as_primary (store);
863 this._set_primary_store (store);
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);
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
879 if (store.is_quiescent == false)
881 this._non_quiescent_persona_store_count++;
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)
887 this._quiescent_timeout_id =
888 Timeout.add_seconds (IndividualAggregator._QUIESCENT_TIMEOUT,
889 this._quiescent_timeout_cb);
893 /* Handle any pre-existing personas in the store. This can happen if the
894 * store existed (and was prepared) before this IndividualAggregator was
896 if (store.personas.size > 0)
898 var persona_set = new HashSet<Persona> ();
899 foreach (var p in store.personas.values)
904 this._personas_changed_cb (store, persona_set,
905 new HashSet<Persona> (), null, null,
906 GroupDetails.ChangeReason.NONE);
909 /* Prepare the store and receive a load of other personas-changed
911 store.prepare.begin ((obj, result) =>
915 store.prepare.end (result);
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,
927 private void _backend_persona_store_removed_cb (Backend backend,
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);
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)
942 this._non_quiescent_persona_store_count--;
943 this._notify_if_is_quiescent ();
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
955 var removed_personas = new HashSet<Persona> ();
956 var iter = store.personas.map_iterator ();
958 while (iter.next () == true)
960 removed_personas.add (iter.get_value ());
962 this._personas_changed_cb (store, new HashSet<Persona> (), removed_personas,
963 null, null, GroupDetails.ChangeReason.NONE);
965 if (this._primary_store == store)
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");
972 this._stores.unset (this._get_store_full_id (store.type_id, store.id));
975 private string _get_store_full_id (string type_id, string id)
977 return type_id + ":" + id;
980 /* Emit the individuals-changed signal ensuring that null parameters are
981 * turned into empty sets, and both sets passed to signal handlers are
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)
990 Set<Individual> _added;
991 Set<Individual> _removed;
992 MultiMap<Individual?, Individual?> _changes;
994 if ((added == null || ((!) added).size == 0) &&
995 (removed == null || ((!) removed).size == 0) &&
996 (changes == null || ((!) changes).size == 0))
998 /* Don't bother emitting it if nothing's changed */
1002 Internal.profiling_point ("emitting " +
1003 "IndividualAggregator::individuals-changed");
1005 _added = (added != null) ? (!) added : new HashSet<Individual> ();
1006 _removed = (removed != null) ? (!) removed : new HashSet<Individual> ();
1008 if (changes != null)
1010 _changes = (!) changes;
1014 _changes = new HashMultiMap<Individual?, Individual?> ();
1018 if (this._debug.debug_output_enabled == true)
1020 debug ("Emitting individuals-changed-detailed with %u mappings:",
1023 foreach (var removed_ind in _changes.get_keys ())
1025 foreach (var added_ind in _changes.get (removed_ind))
1027 debug (" %s (%p) → %s (%p)",
1028 (removed_ind != null) ? ((!) removed_ind).id : "",
1030 (added_ind != null) ? ((!) added_ind).id : "", added_ind);
1032 if (removed_ind != null)
1034 debug (" Removed individual's personas:");
1036 foreach (var p in ((!) removed_ind).personas)
1038 debug (" %s (%p)", p.uid, p);
1042 if (added_ind != null)
1044 debug (" Added individual's personas:");
1046 foreach (var p in ((!) added_ind).personas)
1048 debug (" %s (%p)", p.uid, p);
1055 this.individuals_changed (_added.read_only_view, _removed.read_only_view,
1056 message, actor, reason);
1057 this.individuals_changed_detailed (_changes);
1060 private void _connect_to_individual (Individual individual)
1062 individual.removed.connect (this._individual_removed_cb);
1063 this._individuals.set (individual.id, individual);
1066 private void _disconnect_from_individual (Individual individual)
1068 this._individuals.unset (individual.id);
1069 individual.removed.disconnect (this._individual_removed_cb);
1072 private void _add_personas (Set<Persona> added, ref Individual? user,
1073 ref HashMultiMap<Individual?, Individual?> individuals_changes)
1075 foreach (var persona in added)
1077 PersonaStoreTrust trust_level = persona.store.trust_level;
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> ();
1089 var final_personas = new HashSet<Persona> ();
1091 debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
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)
1098 debug (" Found candidate individual '%s' as user.",
1100 candidate_inds.add ((!) user);
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)
1107 var candidate_ind_set = this._link_map.get (persona.iid);
1108 if (candidate_ind_set != null)
1110 foreach (var candidate_ind in candidate_ind_set)
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))
1118 debug (" Found candidate individual '%s' by " +
1119 "IID '%s'.", ((!) candidate_ind).id, persona.iid);
1125 if (persona.store.trust_level == PersonaStoreTrust.FULL)
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)
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;
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)
1141 /* Translators: the parameter is a property name. */
1142 _("Unknown property '%s' in linkable property list."),
1147 persona.linkable_property_to_links (prop_name, (l) =>
1149 unowned string prop_linking_value = l;
1150 var candidate_ind_set =
1151 this._link_map.get (prop_linking_value);
1153 if (candidate_ind_set != null)
1155 foreach (var candidate_ind in candidate_ind_set)
1157 if (candidate_ind != null &&
1158 ((!) candidate_ind).trust_level !=
1160 ((!) candidate_ind).
1161 has_anti_link_with_persona (
1162 persona) == false &&
1163 candidate_inds.add ((!) candidate_ind))
1165 debug (" Found candidate individual '%s'" +
1166 " by linkable property '%s' = '%s'.",
1167 ((!) candidate_ind).id, prop_name,
1168 prop_linking_value);
1176 /* Ensure the original persona makes it into the final individual */
1177 final_personas.add (persona);
1179 if (candidate_inds.size > 0 && this._linking_enabled == true)
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)
1188 final_personas.add_all (individual.personas);
1191 else if (candidate_inds.size > 0)
1193 debug (" Linking disabled.");
1197 debug (" Did not find any candidate individuals.");
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)
1206 debug (" %s (%p)", p.uid, p);
1207 this._add_persona_to_link_map (p, final_individual);
1210 uint num_mappings_added = 0;
1212 foreach (var i in candidate_inds)
1214 /* Remove the old individuals from the link map. */
1215 this._remove_individual_from_link_map (i);
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?> ();
1222 foreach (var k in individuals_changes.get_keys ())
1224 if (i in individuals_changes.get (k))
1226 transitive_updates.add (k);
1230 foreach (var k in transitive_updates)
1232 assert (individuals_changes.remove (k, i) == true);
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). */
1240 individuals_changes.set (k, final_individual);
1241 num_mappings_added++;
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)
1251 individuals_changes.set (i, final_individual);
1252 num_mappings_added++;
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)
1261 individuals_changes.set (null, final_individual);
1264 /* If the final Individual is the user, set them as such. */
1265 if (final_individual.is_user == true)
1266 user = final_individual;
1270 private void _persona_linkable_property_changed_cb (Object obj,
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);
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);
1283 var persona_set = new HashSet<Persona> ();
1284 persona_set.add (persona);
1286 this._personas_changed_cb (persona.store, persona_set, persona_set,
1287 null, null, GroupDetails.ChangeReason.NONE);
1290 private void _persona_anti_links_changed_cb (Object obj, ParamSpec pspec)
1292 var persona = obj as Persona;
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);
1301 var persona_set = new HashSet<Persona> ();
1302 persona_set.add (persona);
1304 this._personas_changed_cb (persona.store, persona_set, persona_set,
1305 null, null, GroupDetails.ChangeReason.NONE);
1308 private void _connect_to_persona (Persona persona)
1310 foreach (var prop_name in persona.linkable_properties)
1312 persona.notify[prop_name].connect (
1313 this._persona_linkable_property_changed_cb);
1316 var al = persona as AntiLinkable;
1319 al.notify["anti-links"].connect (this._persona_anti_links_changed_cb);
1323 private void _disconnect_from_persona (Persona persona)
1325 var al = persona as AntiLinkable;
1328 al.notify["anti-links"].disconnect (
1329 this._persona_anti_links_changed_cb);
1332 foreach (var prop_name in persona.linkable_properties)
1334 persona.notify[prop_name].disconnect (
1335 this._persona_linkable_property_changed_cb);
1339 private void _add_persona_to_link_map (Persona persona, Individual individual)
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);
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);
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)
1354 debug (" Inserting links:");
1356 /* Insert maps from the Persona's linkable properties to the
1358 foreach (unowned string prop_name in persona.linkable_properties)
1360 debug (" %s", prop_name);
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)
1367 /* Translators: the parameter is a property name. */
1368 _("Unknown property '%s' in linkable property list."),
1373 persona.linkable_property_to_links (prop_name, (l) =>
1375 unowned string prop_linking_value = l;
1377 debug (" %s", prop_linking_value);
1378 this._link_map.set (prop_linking_value, individual);
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)
1392 debug ("Removing Individual '%s' from the link map.", individual.id);
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];
1399 foreach (var link_key in this._link_map.get_keys ())
1401 if (this._link_map.get (link_key).contains (individual))
1403 debug (" %s → %s (%p)",
1404 link_key, individual.id, individual);
1406 remove_keys += link_key;
1410 /* Actually remove the keys. */
1411 foreach (var link_key in remove_keys)
1413 this._link_map.remove (link_key, individual);
1417 private void _personas_changed_cb (PersonaStore store,
1419 Set<Persona> removed,
1422 GroupDetails.ChangeReason reason)
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> ();
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
1432 var user = this.user;
1434 debug ("Removing Personas:");
1436 foreach (var persona in removed)
1438 debug (" %s (is user: %s, IID: %s)", persona.uid,
1439 persona.is_user ? "yes" : "no", persona.iid);
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;
1447 removed_individuals.add ((!) ind);
1450 /* Stop listening to notifications about the persona's linkable
1452 this._disconnect_from_persona (persona);
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)
1465 /* Ensure we don't remove the same Individual twice */
1466 if (this._individuals.has_key (individual.id) == false)
1469 debug (" %s", individual.id);
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)
1475 if (removed.contains (persona) == true ||
1476 relinked_personas.contains (persona) == true)
1479 relinked_personas.add (persona);
1482 if (user == individual)
1485 this._disconnect_from_individual (individual);
1487 /* Remove the Individual's links from the link map */
1488 this._remove_individual_from_link_map (individual);
1491 debug ("Adding Personas:");
1492 foreach (var persona in added)
1494 debug (" %s (is user: %s, IID: %s)", persona.uid,
1495 persona.is_user ? "yes" : "no", persona.iid);
1497 /* Connect to notifications about the persona's linkable
1499 this._connect_to_persona (persona);
1504 this._add_personas (added, ref user, ref individuals_changes);
1507 debug ("Relinking Personas:");
1508 foreach (var persona in relinked_personas)
1510 debug (" %s (is user: %s, IID: %s)", persona.uid,
1511 persona.is_user ? "yes" : "no", persona.iid);
1514 this._add_personas (relinked_personas, ref user, ref individuals_changes);
1516 /* Work out which final individuals have replaced the removed_individuals
1517 * and update individuals_changes accordingly. */
1518 foreach (var individual in removed_individuals)
1520 var added_mapping = false;
1522 foreach (var persona in individual.personas)
1524 if (!(persona in removed) || (persona in added))
1526 individuals_changes.remove (null, persona.individual);
1527 individuals_changes.set (individual, persona.individual);
1528 added_mapping = true;
1532 /* Has the individual been removed entirely? */
1533 if (added_mapping == false)
1535 individuals_changes.set (individual, null);
1538 individual.personas = null;
1541 /* Notify of changes to this.user */
1544 /* Signal the addition of new individuals and removal of old ones to the
1546 if (individuals_changes.size > 0)
1548 var added_individuals = new HashSet<Individual> ();
1550 /* Extract the deprecated added and removed sets from
1551 * individuals_changes, to be used in the individuals_changed
1553 foreach (var old_ind in individuals_changes.get_keys ())
1555 foreach (var new_ind in individuals_changes.get (old_ind))
1557 assert (old_ind != null || new_ind != null);
1559 if (old_ind != null)
1561 removed_individuals.add ((!) old_ind);
1564 if (new_ind != null)
1566 added_individuals.add ((!) new_ind);
1567 this._connect_to_individual ((!) new_ind);
1570 if (old_ind != null && new_ind != null)
1572 replaced_individuals.set ((!) old_ind, (!) new_ind);
1577 this._emit_individuals_changed (added_individuals,
1578 removed_individuals, individuals_changes);
1581 /* Signal the replacement of various Individuals as a consequence of
1583 debug ("Replacing Individuals due to linking:");
1584 var iter = replaced_individuals.map_iterator ();
1585 while (iter.next () == true)
1587 var old_ind = iter.get_key ();
1588 var new_ind = iter.get_value ();
1590 debug (" %s (%p) → %s (%p)", old_ind.id, old_ind,
1591 new_ind.id, new_ind);
1593 old_ind.replace (new_ind);
1596 /* Validate the link map. */
1597 if (this._debug.debug_output_enabled == true)
1599 foreach (var link_key in this._link_map.get_keys ())
1601 foreach (var individual in this._link_map.get (link_key))
1603 if (this._individuals.get (individual.id) != individual)
1605 warning ("Link map contains invalid mapping:\n" +
1607 link_key, individual.id, individual);
1608 warning ("Individual %s (%p) personas:", individual.id,
1610 foreach (var p in individual.personas)
1612 warning (" %s (%p)", p.uid, p);
1620 private void _is_primary_store_changed_cb (Object object, ParamSpec pspec)
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));
1630 private void _persona_store_is_quiescent_changed_cb (Object obj,
1633 /* Have we reached a quiescent state yet? */
1634 if (this._non_quiescent_persona_store_count > 0)
1636 this._non_quiescent_persona_store_count--;
1637 this._notify_if_is_quiescent ();
1641 private void _backend_is_quiescent_changed_cb (Object obj, ParamSpec pspec)
1643 if (this._non_quiescent_backend_count > 0)
1645 this._non_quiescent_backend_count--;
1646 this._notify_if_is_quiescent ();
1650 private void _notify_if_is_quiescent ()
1652 if (this._non_quiescent_backend_count == 0 &&
1653 this._non_quiescent_persona_store_count == 0 &&
1654 this._is_quiescent == false)
1656 if (this._configured_primary_store_type_id.length > 0 &&
1657 this._primary_store == null)
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 " +
1666 this._configured_primary_store_type_id,
1667 this._configured_primary_store_id);
1670 Internal.profiling_point ("reached quiescence in " +
1671 "IndividualAggregator");
1673 this._is_quiescent = true;
1674 this.notify_property ("is-quiescent");
1676 /* Remove the quiescence timeout, if it exists. */
1677 if (this._quiescent_timeout_id != 0)
1679 Source.remove (this._quiescent_timeout_id);
1680 this._quiescent_timeout_id = 0;
1685 private bool _quiescent_timeout_cb ()
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)
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);
1698 this._is_quiescent = true;
1699 this.notify_property ("is-quiescent");
1702 /* One-shot timeout */
1703 this._quiescent_timeout_id = 0;
1707 private void _persona_store_is_user_set_default_changed_cb (Object obj,
1710 var store = (PersonaStore) obj;
1712 debug ("PersonaStore.is-user-set-default changed for store %p " +
1713 "(type ID: %s, ID: %s)", store, store.type_id, store.id);
1715 if (this._maybe_configure_as_primary (store))
1716 this._set_primary_store (store);
1719 private bool _maybe_configure_as_primary (PersonaStore store)
1721 debug ("_maybe_configure_as_primary()");
1723 var configured = false;
1725 if (!this._user_configured_primary_store &&
1726 store.is_user_set_default)
1728 debug ("Setting primary store IDs to '%s' and '%s'.", store.type_id,
1730 this._configured_primary_store_type_id = store.type_id;
1731 this._configured_primary_store_id = store.id;
1738 private void _individual_removed_cb (Individual i, Individual? replacement)
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)
1748 if (replacement != null)
1750 debug ("Individual '%s' removed (replaced by '%s')", i.id,
1751 ((!) replacement).id);
1755 debug ("Individual '%s' removed (not replaced)", i.id);
1758 /* If the individual has 0 personas, we've already signaled removal */
1759 if (i.personas.size > 0)
1761 var changes = new HashMultiMap<Individual?, Individual?> ();
1762 var individuals = new HashSet<Individual> ();
1764 individuals.add (i);
1765 changes.set (i, replacement);
1767 this._emit_individuals_changed (null, individuals, changes);
1770 this._disconnect_from_individual (i);
1774 * Add a new persona in the given {@link PersonaStore} based on the
1775 * ``details`` provided.
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.
1782 * The details hash is a backend-specific mapping of key, value strings.
1783 * Common keys include:
1785 * * contact - service-specific contact ID
1786 * * message - a user-readable message to pass to the persona being added
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``.
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
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
1801 * @throws IndividualAggregatorError.ADD_FAILED if any other error occurred
1802 * while adding the persona
1806 public async Persona? add_persona_from_details (Individual? parent,
1807 PersonaStore persona_store,
1808 HashTable<string, Value?> details) throws IndividualAggregatorError
1810 Persona? persona = null;
1813 var details_copy = this._asv_copy (details);
1814 persona = yield persona_store.add_persona_from_details (details_copy);
1816 catch (PersonaStoreError e)
1818 if (e is PersonaStoreError.STORE_OFFLINE)
1820 throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
1824 var full_id = this._get_store_full_id (persona_store.type_id,
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);
1835 if (parent != null && persona != null)
1837 ((!) parent).personas.add ((!) persona);
1843 private HashTable<string, Value?> _asv_copy (HashTable<string, Value?> asv)
1845 var retval = new HashTable<string, Value?> (str_hash, str_equal);
1847 asv.foreach ((k, v) =>
1849 retval.insert ((string) k, v);
1856 * Completely remove the individual and all of its personas from their
1859 * This method is safe to call multiple times concurrently (for the same
1860 * individual or different individuals).
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}
1868 public async void remove_individual (Individual individual) throws GLib.Error
1870 /* Removing personas changes the persona set so we need to make a copy
1872 var personas = new HashSet<Persona> ();
1873 foreach (var p in individual.personas)
1878 foreach (var persona in personas)
1880 yield persona.store.remove_persona (persona);
1885 * Completely remove the persona from its backing store.
1887 * This will leave other personas in the same individual alone.
1889 * This method is safe to call multiple times concurrently (for the same
1890 * persona or different personas).
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}
1898 public async void remove_persona (Persona persona) throws GLib.Error
1900 yield persona.store.remove_persona (persona);
1904 * Link the given {@link Persona}s together.
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.
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}.
1914 * This method is safe to call multiple times concurrently.
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}
1925 public async void link_personas (Set<Persona> personas)
1926 throws IndividualAggregatorError
1928 if (this._primary_store == null)
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));
1940 /* Don't bother linking if it's just one Persona */
1941 if (personas.size <= 1)
1944 /* Disallow linking if it's disabled */
1945 if (this._linking_enabled == false)
1947 debug ("Can't link Personas: linking disabled.");
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)
1955 var al = p as AntiLinkable;
1960 yield ((!) al).remove_anti_links (personas);
1962 catch (PropertyError e)
1964 throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
1965 _("Anti-links can't be removed between personas being linked."));
1970 /* Create a new persona in the primary store which links together the
1972 assert (((!) this._primary_store).type_id ==
1973 this._configured_primary_store_type_id);
1975 var details = this._build_linking_details (personas);
1977 yield this.add_persona_from_details (null,
1978 (!) this._primary_store, details);
1981 private HashTable<string, Value?> _build_linking_details (
1982 Set<Persona> personas)
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);
1993 /* List of local_ids */
1994 var local_ids = new Gee.HashSet<string> ();
1996 foreach (var persona in personas)
1998 if (persona is ImDetails)
2000 ImDetails im_details = (ImDetails) persona;
2002 /* protocols_addrs_set = union (all personas' IM addresses) */
2003 foreach (var protocol in im_details.im_addresses.get_keys ())
2005 var im_addresses = im_details.im_addresses.get (protocol);
2007 foreach (var im_address in im_addresses)
2009 protocols_addrs_set.set (protocol, im_address);
2014 if (persona is WebServiceDetails)
2016 WebServiceDetails ws_details = (WebServiceDetails) persona;
2018 /* web_service_addrs_set = union (all personas' WS addresses) */
2019 foreach (var web_service in
2020 ws_details.web_service_addresses.get_keys ())
2023 ws_details.web_service_addresses.get (web_service);
2025 foreach (var ws_fd in ws_addresses)
2026 web_service_addrs_set.set (web_service, ws_fd);
2030 if (persona is LocalIdDetails)
2032 foreach (var id in ((LocalIdDetails) persona).local_ids)
2039 var details = new HashTable<string, Value?> (str_hash, str_equal);
2041 if (protocols_addrs_set.size > 0)
2043 var im_addresses_value = Value (typeof (MultiMap));
2044 im_addresses_value.set_object (protocols_addrs_set);
2046 (!) PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES),
2047 im_addresses_value);
2050 if (web_service_addrs_set.size > 0)
2052 var web_service_addresses_value = Value (typeof (MultiMap));
2053 web_service_addresses_value.set_object (web_service_addrs_set);
2055 (!) PersonaStore.detail_key (PersonaDetail.WEB_SERVICE_ADDRESSES),
2056 web_service_addresses_value);
2059 if (local_ids.size > 0)
2061 var local_ids_value = Value (typeof (Set));
2062 local_ids_value.set_object (local_ids);
2064 (!) Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
2072 * Unlinks the given {@link Individual} into its constituent {@link Persona}s.
2074 * This completely unlinks the given {@link Individual}, destroying all of
2075 * its writeable {@link Persona}s.
2077 * The {@link Individual}'s removal is signalled by
2078 * {@link IndividualAggregator.individuals_changed} and
2079 * {@link Individual.removed}.
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}.
2087 * This method is safe to call multiple times concurrently, although
2088 * concurrent calls for the same individual may result in duplicate personas
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}
2097 public async void unlink_individual (Individual individual) throws GLib.Error
2099 if (this._linking_enabled == false)
2101 debug ("Can't unlink Individual '%s': linking disabled.",
2106 debug ("Unlinking Individual '%s':", individual.id);
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.
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.
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);
2122 debug (" Inserting anti-links:");
2123 foreach (var pers in individual_personas)
2127 var personas = new HashSet<Persona> ();
2128 personas.add (pers);
2129 debug (" Anti-linking persona '%s' (%p)", pers.uid, pers);
2131 var writeable_persona =
2132 yield this._ensure_personas_property_writeable (personas,
2134 debug (" Writeable persona '%s' (%p)",
2135 writeable_persona.uid, writeable_persona);
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);
2142 var al = writeable_persona as AntiLinkable;
2143 assert (al != null);
2144 yield ((!) al).add_anti_links (anti_link_personas);
2147 catch (IndividualAggregatorError e1)
2149 debug (" Failed to ensure anti-links property is writeable " +
2150 "(continuing anyway): %s", e1.message);
2156 * Ensure that the given property is writeable for the given
2157 * {@link Individual}.
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.)
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
2173 * This method is safe to call multiple times concurrently, although
2174 * concurrent calls for the same individual may result in duplicate personas
2177 * @param individual the individual for which ``property_name`` should be
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
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
2193 public async Persona ensure_individual_property_writeable (
2194 Individual individual, string property_name)
2195 throws IndividualAggregatorError
2197 debug ("ensure_individual_property_writeable: %s, %s",
2198 individual.id, property_name);
2200 var p = yield this._ensure_personas_property_writeable (
2201 individual.personas, property_name);
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
2212 /* See if the persona set already contains the property we want. */
2213 foreach (var p1 in personas)
2215 if (property_name in p1.writeable_properties)
2217 debug (" Returning existing persona: %s", p1.uid);
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;
2228 if (this._primary_store != null &&
2230 ((!) this._primary_store).always_writeable_properties)
2234 debug (" Using writeable store");
2235 new_persona = yield this.add_persona_from_details (null,
2236 (!) this._primary_store, details);
2238 catch (IndividualAggregatorError e1)
2245 if (new_persona == null)
2247 foreach (var s in this._stores.values)
2249 if (s == this._primary_store ||
2250 !(property_name in s.always_writeable_properties))
2252 /* Skip the store we've just tried */
2258 debug (" Using store %s", s.id);
2259 new_persona = yield this.add_persona_from_details (null, s,
2262 catch (IndividualAggregatorError e2)
2271 /* Throw an error if we haven't managed to find a suitable store */
2272 if (new_persona == null && this._primary_store == null)
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));
2283 else if (new_persona == null)
2285 throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
2286 _("Can't write to requested property (“%s”) of the writeable store."),
2290 /* We can guarantee new_persona != null because we'd have bailed out above
2292 return (!) new_persona;
2296 * Look up an individual in the aggregator.
2298 * This returns the {@link Individual} with the given ``id`` if it exists in
2299 * the aggregator, and ``null`` otherwise.
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.
2305 * This method is safe to call before {@link IndividualAggregator.prepare} has
2306 * been called, and will call {@link IndividualAggregator.prepare} itself in
2309 * This method is safe to call multiple times concurrently.
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}
2317 public async Individual? look_up_individual (string id) throws GLib.Error
2319 /* Ensure the aggregator's prepared. */
2320 yield this.prepare ();
2322 /* FIXME: When bgo#648805 is fixed, this needs to support lazy-loading. */
2323 return this._individuals.get (id);