/*
* Copyright (C) 2010 Collabora Ltd.
*
* This library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this library. If not, see .
*
* Authors:
* Travis Reitter
*/
using Gee;
using GLib;
/**
* Errors from {@link IndividualAggregator}s.
*/
public errordomain Folks.IndividualAggregatorError
{
/**
* Adding a {@link Persona} to a {@link PersonaStore} failed.
*/
ADD_FAILED,
/**
* An operation which required the use of a writeable store failed because no
* writeable store was available.
*
* @since 0.1.13
*/
[Deprecated (since = "0.6.2.1",
replacement = "IndividualAggregatorError.NO_PRIMARY_STORE")]
NO_WRITEABLE_STORE,
/**
* The {@link PersonaStore} was offline (ie, this is a temporary failure).
*
* @since 0.3.0
*/
STORE_OFFLINE,
/**
* The {@link PersonaStore} did not support writing to a property which the
* user requested to write to, or which was necessary to write to for storing
* linking information.
*
* @since 0.6.2
*/
PROPERTY_NOT_WRITEABLE,
/**
* An operation which required the use of a primary store failed because no
* primary store was available.
*
* @since 0.6.3
*/
NO_PRIMARY_STORE,
}
/**
* Stores {@link Individual}s which have been created through
* aggregation of all the {@link Persona}s provided by the various
* {@link Backend}s.
*
* This is the main interface for client applications.
*/
public class Folks.IndividualAggregator : Object
{
private BackendStore _backend_store;
private HashMap _stores;
private unowned PersonaStore? _primary_store = null;
private HashSet _backends;
private HashTable _link_map;
private bool _linking_enabled = true;
private bool _is_prepared = false;
private bool _prepare_pending = false;
private Debug _debug;
private string _configured_primary_store_type_id;
private string _configured_primary_store_id;
private static const string _FOLKS_CONFIG_KEY =
"/system/folks/backends/primary_store";
/* The number of persona stores and backends we're waiting to become
* quiescent. Once these both reach 0, we should be in a quiescent state.
* We have to count both of them so that we can handle the case where one
* backend becomes available, and its persona stores all become quiescent,
* long before any other backend becomes available. In this case, we want
* the aggregator to signal that it's reached a quiescent state only once
* all the other backends have also become available. */
private uint _non_quiescent_persona_store_count = 0;
/* Same for backends. */
private uint _non_quiescent_backend_count = 0;
private bool _is_quiescent = false;
/* We use this to know if the primary PersonaStore has been explicitly
* set by the user (either via GConf or an env variable). If that is the
* case, we don't want to override it with other PersonaStores that
* announce themselves as default (i.e.: default address book from e-d-s). */
private bool _user_configured_primary_store = false;
/**
* Whether {@link IndividualAggregator.prepare} has successfully completed for
* this aggregator.
*
* @since 0.3.0
*/
public bool is_prepared
{
get { return this._is_prepared; }
}
/**
* Whether the aggregator has reached a quiescent state. This will happen at
* some point after {@link IndividualAggregator.prepare} has successfully
* completed for the aggregator. An aggregator is in a quiescent state when
* all the {@link PersonaStore}s listed by its backends have reached a
* quiescent state.
*
* It's guaranteed that this property's value will only ever change after
* {@link IndividualAggregator.is_prepared} has changed to `true`.
*
* @since 0.6.2
*/
public bool is_quiescent
{
get { return this._is_quiescent; }
}
/**
* Our configured primary (writeable) store.
*
* Which one to use is decided (in order or precedence)
* by:
*
* - the FOLKS_PRIMARY_STORE env var (mostly for debugging)
* - the GConf key set in _FOLKS_CONFIG_KEY (system set store)
* - going with the `key-file` or `eds` store as the fall-back option
*
* @since 0.5.0
*/
public PersonaStore? primary_store
{
get { return this._primary_store; }
}
private Map _individuals;
private Map _individuals_ro;
/**
* A map from {@link Individual.id}s to their {@link Individual}s.
*
* This is the canonical set of {@link Individual}s provided by this
* IndividualAggregator.
*
* {@link Individual}s may be added or removed using
* {@link IndividualAggregator.add_persona_from_details} and
* {@link IndividualAggregator.remove_individual}, respectively.
*
* @since 0.5.1
*/
public Map individuals
{
get { return this._individuals_ro; }
private set
{
this._individuals = value;
this._individuals_ro = this._individuals.read_only_view;
}
}
/**
* The {@link Individual} representing the user.
*
* If it exists, this holds the {@link Individual} who is the user: the
* {@link Individual} containing the {@link Persona}s who are the owners of
* the accounts for their respective backends.
*
* @since 0.3.0
*/
public Individual? user { get; private set; }
/**
* Emitted when one or more {@link Individual}s are added to or removed from
* the aggregator.
*
* If more information about the relationships between {@link Individual}s
* which have been linked and unlinked is needed, consider connecting to
* {@link IndividualAggregator.individuals_changed_detailed} instead, which is
* emitted at the same time as this signal.
*
* This will not be emitted until after {@link IndividualAggregator.prepare}
* has been called.
*
* @param added a list of {@link Individual}s which have been added
* @param removed a list of {@link Individual}s which have been removed
* @param message a string message from the backend, if any
* @param actor the {@link Persona} who made the change, if known
* @param reason the reason for the change
*
* @since 0.5.1
*/
[Deprecated (since = "0.6.2",
replacement = "IndividualAggregator.individuals_changed_detailed")]
public signal void individuals_changed (Set added,
Set removed,
string? message,
Persona? actor,
GroupDetails.ChangeReason reason);
/**
* Emitted when one or more {@link Individual}s are added to or removed from
* the aggregator.
*
* This is emitted at the same time as
* {@link IndividualAggregator.individuals_changed}, but includes more
* information about the relationships between {@link Individual}s which have
* been linked and unlinked.
*
* Individuals which have been linked will be listed in the multi-map as
* mappings from the old individuals to the single new individual which
* replaces them (i.e. each of the old individuals will map to the same new
* individual). This new individual is the one which will be specified as the
* `replacement_individual` in the {@link Individual.removed} signal for the
* old individuals.
*
* Individuals which have been unlinked will be listed in the multi-map as
* a mapping from the unlinked individual to a set of one or more individuals
* which replace it.
*
* Individuals which have been added will be listed in the multi-map as a
* mapping from `null` to the set of added individuals. If `null` doesn't
* map to anything, no individuals have been added to the aggregator.
*
* Individuals which have been removed will be listed in the multi-map as
* mappings from the removed individual to `null`.
*
* This will not be emitted until after {@link IndividualAggregator.prepare}
* has been called.
*
* @param changes a mapping of old {@link Individual}s to new
* {@link Individual}s for the individuals which have changed in the
* aggregator
*
* @since 0.6.2
*/
public signal void individuals_changed_detailed (
MultiMap changes);
/* FIXME: make this a singleton? */
/**
* Create a new IndividualAggregator.
*
* Clients should connect to the
* {@link IndividualAggregator.individuals_changed} signal (or the
* {@link IndividualAggregator.individuals_changed_detailed} signal), then
* call {@link IndividualAggregator.prepare} to load the backends and start
* aggregating individuals.
*
* An example of how to set up an IndividualAggregator:
* {{{
* IndividualAggregator agg = new IndividualAggregator ();
* agg.individuals_changed_detailed.connect (individuals_changed_cb);
* agg.prepare ();
* }}}
*/
public IndividualAggregator ()
{
Object ();
}
construct
{
this._stores = new HashMap ();
this._individuals = new HashMap ();
this._individuals_ro = this._individuals.read_only_view;
this._link_map = new HashTable (str_hash, str_equal);
this._backends = new HashSet ();
this._debug = Debug.dup ();
this._debug.print_status.connect (this._debug_print_status);
/* Check out the configured primary store */
var store_config_ids = Environment.get_variable ("FOLKS_PRIMARY_STORE");
if (store_config_ids == null)
{
store_config_ids = Environment.get_variable ("FOLKS_WRITEABLE_STORE");
if (store_config_ids != null)
{
var deprecated_warn = "FOLKS_WRITEABLE_STORE is deprecated, ";
deprecated_warn += "use FOLKS_PRIMARY_STORE";
warning (deprecated_warn);
}
}
if (store_config_ids != null)
{
debug ("Setting primary store IDs from environment variable.");
this._configure_primary_store ((!) store_config_ids);
}
else
{
debug ("Setting primary store IDs to defaults.");
if (BuildConf.HAVE_EDS)
{
this._configured_primary_store_type_id = "eds";
this._configured_primary_store_id = "system";
}
else
{
this._configured_primary_store_type_id = "key-file";
this._configured_primary_store_id = "";
}
try
{
unowned GConf.Client client = GConf.Client.get_default ();
GConf.Value? val = client.get (this._FOLKS_CONFIG_KEY);
if (val != null)
{
string? val_str = ((!) val).get_string ();
if (val_str != null)
{
debug ("Setting primary store IDs from GConf.");
this._configure_primary_store ((!) val_str);
}
}
}
catch (GLib.Error e)
{
/* We ignore errors and go with the default store */
}
}
debug ("Primary store IDs are '%s' and '%s'.",
this._configured_primary_store_type_id,
this._configured_primary_store_id);
var disable_linking = Environment.get_variable ("FOLKS_DISABLE_LINKING");
if (disable_linking != null)
disable_linking = ((!) disable_linking).strip ().down ();
this._linking_enabled = (disable_linking == null ||
disable_linking == "no" || disable_linking == "0");
this._backend_store = BackendStore.dup ();
debug ("Constructing IndividualAggregator %p", this);
}
~IndividualAggregator ()
{
debug ("Destroying IndividualAggregator %p", this);
this._backend_store.backend_available.disconnect (
this._backend_available_cb);
this._debug.print_status.disconnect (this._debug_print_status);
}
private void _configure_primary_store (string store_config_ids)
{
debug ("_configure_primary_store to '%s'", store_config_ids);
this._user_configured_primary_store = true;
if (store_config_ids.index_of (":") != -1)
{
var ids = store_config_ids.split (":", 2);
this._configured_primary_store_type_id = ids[0];
this._configured_primary_store_id = ids[1];
}
else
{
this._configured_primary_store_type_id = store_config_ids;
this._configured_primary_store_id = "";
}
}
private void _debug_print_status (Debug debug)
{
const string domain = Debug.STATUS_LOG_DOMAIN;
const LogLevelFlags level = LogLevelFlags.LEVEL_INFO;
debug.print_heading (domain, level, "IndividualAggregator (%p)", this);
debug.print_key_value_pairs (domain, level,
"Ref. count", this.ref_count.to_string (),
"Primary store", "%p".printf (this._primary_store),
"Configured store type id", this._configured_primary_store_type_id,
"Configured store id", this._configured_primary_store_id,
"Linking enabled?", this._linking_enabled ? "yes" : "no",
"Prepared?", this._is_prepared ? "yes" : "no",
"Quiescent?", this._is_quiescent
? "yes"
: "no (%u backends, %u persona stores left)".printf (
this._non_quiescent_backend_count,
this._non_quiescent_persona_store_count)
);
debug.print_line (domain, level,
"%u Individuals:", this.individuals.size);
debug.indent ();
foreach (var individual in this.individuals.values)
{
string? trust_level = null;
switch (individual.trust_level)
{
case TrustLevel.NONE:
trust_level = "none";
break;
case TrustLevel.PERSONAS:
trust_level = "personas";
break;
default:
assert_not_reached ();
}
debug.print_heading (domain, level, "Individual (%p)", individual);
debug.print_key_value_pairs (domain, level,
"Ref. count", individual.ref_count.to_string (),
"ID", individual.id,
"User?", individual.is_user ? "yes" : "no",
"Trust level", trust_level
);
debug.print_line (domain, level, "%u Personas:",
individual.personas.size);
debug.indent ();
foreach (var persona in individual.personas)
{
debug.print_heading (domain, level, "Persona (%p)", persona);
debug.print_key_value_pairs (domain, level,
"Ref. count", persona.ref_count.to_string (),
"UID", persona.uid,
"IID", persona.iid,
"Display ID", persona.display_id,
"User?", persona.is_user ? "yes" : "no"
);
}
debug.unindent ();
}
debug.unindent ();
debug.print_line (domain, level, "%u entries in the link map:",
this._link_map.size ());
debug.indent ();
var iter = HashTableIter (this._link_map);
string link_key;
Individual individual;
while (iter.next (out link_key, out individual) == true)
{
debug.print_line (domain, level,
"%s → %p", link_key, individual);
}
debug.unindent ();
debug.print_line (domain, level, "");
}
/**
* Prepare the IndividualAggregator for use.
*
* This loads all the available backends and prepares them for use by the
* IndividualAggregator. This should be called //after// connecting to the
* {@link IndividualAggregator.individuals_changed} signal (or
* {@link IndividualAggregator.individuals_changed_detailed} signal), or a
* race condition could occur, with the signal being emitted before your code
* has connected to them, and {@link Individual}s getting "lost" as a result.
*
* This function is guaranteed to be idempotent (since version 0.3.0).
*
* Concurrent calls to this function from different threads will block until
* preparation has completed. However, concurrent calls to this function from
* a single thread might not, i.e. the first call will block but subsequent
* calls might return before the first one. (Though they will be safe in every
* other respect.)
*
* @since 0.1.11
*/
public async void prepare () throws GLib.Error
{
/* Once this async function returns, all the {@link Backend}s will have
* been prepared (though no {@link PersonaStore}s are guaranteed to be
* available yet). This last guarantee is new as of version 0.2.0. */
lock (this._is_prepared)
{
if (this._is_prepared || this._prepare_pending)
{
return;
}
try
{
this._prepare_pending = true;
this._backend_store.backend_available.connect (
this._backend_available_cb);
/* Load any backends which already exist. This could happen if the
* BackendStore has stayed alive after being used by a previous
* IndividualAggregator instance. */
var backends = this._backend_store.enabled_backends.values;
foreach (var backend in backends)
{
this._backend_available_cb (this._backend_store, backend);
}
/* Load any backends which haven't been loaded already. (Typically
* all of them.) */
yield this._backend_store.load_backends ();
this._is_prepared = true;
this.notify_property ("is-prepared");
/* Mark the aggregator as having reached a quiescent state if
* appropriate. This will typically only happen here in cases
* where the stores were all prepared and quiescent before the
* aggregator was created. */
if (this._is_quiescent == false)
{
this._notify_if_is_quiescent ();
}
}
finally
{
this._prepare_pending = false;
}
}
}
/**
* Get all matches for a given {@link Individual}.
*
* @param matchee the individual to find matches for
* @param min_threshold the threshold for accepting a match
* @return a map from matched individuals to the degree with which they match
* `matchee` (which is guaranteed to at least equal `min_threshold`);
* if no matches could be found, an empty map is returned
*
* @since 0.5.1
*/
public Map get_potential_matches
(Individual matchee, MatchResult min_threshold = MatchResult.VERY_HIGH)
{
HashMap matches =
new HashMap ();
Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
foreach (var i in this._individuals.values)
{
if (i.id == matchee.id)
continue;
var result = matchObj.potential_match (i, matchee);
if (result >= min_threshold)
{
matches.set (i, result);
}
}
return matches;
}
/**
* Get all combinations between all {@link Individual}s.
*
* @param min_threshold the threshold for accepting a match
* @return a map from each individual in the aggregator to a map of the
* other individuals in the aggregator which can be matched with that
* individual, mapped to the degree with which they match the original
* individual (which is guaranteed to at least equal `min_threshold`)
*
* @since 0.5.1
*/
public Map>
get_all_potential_matches
(MatchResult min_threshold = MatchResult.VERY_HIGH)
{
HashMap> matches =
new HashMap> ();
var individuals = this._individuals.values.to_array ();
Folks.PotentialMatch matchObj = new Folks.PotentialMatch ();
for (var i = 0; i < individuals.length; i++)
{
var a = individuals[i];
HashMap? _matches_a = matches.get (a);
HashMap matches_a;
if (_matches_a == null)
{
matches_a = new HashMap ();
matches.set (a, matches_a);
}
else
{
matches_a = (!) _matches_a;
}
for (var f = i + 1; f < individuals.length; f++)
{
var b = individuals[f];
HashMap? _matches_b = matches.get (b);
HashMap matches_b;
if (_matches_b == null)
{
matches_b = new HashMap ();
matches.set (b, matches_b);
}
else
{
matches_b = (!) _matches_b;
}
var result = matchObj.potential_match (a, b);
if (result >= min_threshold)
{
matches_a.set (b, result);
matches_b.set (a, result);
}
}
}
return matches;
}
private async void _add_backend (Backend backend)
{
if (!this._backends.contains (backend))
{
this._backends.add (backend);
backend.persona_store_added.connect (
this._backend_persona_store_added_cb);
backend.persona_store_removed.connect (
this._backend_persona_store_removed_cb);
backend.notify["is-quiescent"].connect (
this._backend_is_quiescent_changed_cb);
/* Handle the stores that have already been signaled. Since
* this might change while we are looping, get a copy first.
*/
var stores = backend.persona_stores.values.to_array ();
foreach (var persona_store in stores)
{
this._backend_persona_store_added_cb (backend, persona_store);
}
}
}
private void _backend_available_cb (BackendStore backend_store,
Backend backend)
{
/* Increase the number of non-quiescent backends we're waiting for.
* If we've already reached a quiescent state, this is ignored. If we
* haven't, this delays us reaching a quiescent state until the
* _backend_is_quiescent_changed_cb() callback is called for this
* backend. */
if (backend.is_quiescent == false)
{
this._non_quiescent_backend_count++;
}
this._add_backend.begin (backend);
}
private void _set_primary_store (PersonaStore store)
{
debug ("_set_primary_store()");
if (this._primary_store == store)
return;
/* We use the configured PersonaStore as the primary PersonaStore.
*
* If the type_id is `eds` we *must* know the actual store
* (address book) we are talking about or we might end up using
* a random store on every run.
*/
if (store.type_id == this._configured_primary_store_type_id)
{
if ((store.type_id != "eds" &&
this._configured_primary_store_id == "") ||
this._configured_primary_store_id == store.id)
{
debug ("Setting primary store to %p (type ID: %s, ID: %s)",
store, store.type_id, store.id);
var previous_store = this._primary_store;
this._primary_store = store;
store.freeze_notify ();
if (previous_store != null)
{
((!) previous_store).freeze_notify ();
((!) previous_store).is_primary_store = false;
}
store.is_primary_store = true;
if (previous_store != null)
((!) previous_store).thaw_notify ();
store.thaw_notify ();
this.notify_property ("primary-store");
}
}
}
private void _backend_persona_store_added_cb (Backend backend,
PersonaStore store)
{
debug ("_backend_persona_store_added_cb(): backend: %s, store: %s (%p)",
backend.name, store.id, store);
var store_id = this._get_store_full_id (store.type_id, store.id);
this._maybe_configure_as_primary (store);
this._set_primary_store (store);
this._stores.set (store_id, store);
store.personas_changed.connect (this._personas_changed_cb);
store.notify["is-primary-store"].connect (
this._is_primary_store_changed_cb);
store.notify["is-quiescent"].connect (
this._persona_store_is_quiescent_changed_cb);
store.notify["is-user-set-default"].connect (
this._persona_store_is_user_set_default_changed_cb);
/* Increase the number of non-quiescent persona stores we're waiting for.
* If we've already reached a quiescent state, this is ignored. If we
* haven't, this delays us reaching a quiescent state until the
* _persona_store_is_quiescent_changed_cb() callback is called for this
* store. */
if (store.is_quiescent == false)
{
this._non_quiescent_persona_store_count++;
}
/* Handle any pre-existing personas in the store. This can happen if the
* store existed (and was prepared) before this IndividualAggregator was
* constructed. */
if (store.personas.size > 0)
{
var persona_set = new HashSet ();
foreach (var p in store.personas.values)
{
persona_set.add (p);
}
this._personas_changed_cb (store, persona_set,
new HashSet (), null, null,
GroupDetails.ChangeReason.NONE);
}
/* Prepare the store and receive a load of other personas-changed
* signals. */
store.prepare.begin ((obj, result) =>
{
try
{
store.prepare.end (result);
}
catch (GLib.Error e)
{
/* Translators: the first parameter is a persona store identifier
* and the second is an error message. */
warning (_("Error preparing persona store '%s': %s"), store_id,
e.message);
}
});
}
private void _backend_persona_store_removed_cb (Backend backend,
PersonaStore store)
{
store.personas_changed.disconnect (this._personas_changed_cb);
store.notify["is-quiescent"].disconnect (
this._persona_store_is_quiescent_changed_cb);
store.notify["is-primary-store"].disconnect (
this._is_primary_store_changed_cb);
store.notify["is-user-set-default"].disconnect (
this._persona_store_is_user_set_default_changed_cb);
/* If we were still waiting on this persona store to reach a quiescent
* state, stop waiting. */
if (this._is_quiescent == false && store.is_quiescent == false)
{
this._non_quiescent_persona_store_count--;
this._notify_if_is_quiescent ();
}
/* no need to remove this store's personas from all the individuals, since
* they'll do that themselves (and emit their own 'removed' signal if
* necessary) */
if (this._primary_store == store)
{
debug ("Unsetting primary store as store %p (type ID: %s, ID: %s) " +
"has been removed", store, store.type_id, store.id);
this._primary_store = null;
this.notify_property ("primary-store");
}
this._stores.unset (this._get_store_full_id (store.type_id, store.id));
}
private string _get_store_full_id (string type_id, string id)
{
return type_id + ":" + id;
}
/* Emit the individuals-changed signal ensuring that null parameters are
* turned into empty sets, and both sets passed to signal handlers are
* read-only. */
private void _emit_individuals_changed (Set? added,
Set? removed,
MultiMap? changes,
string? message = null,
Persona? actor = null,
GroupDetails.ChangeReason reason = GroupDetails.ChangeReason.NONE)
{
Set _added;
Set _removed;
MultiMap _changes;
if ((added == null || ((!) added).size == 0) &&
(removed == null || ((!) removed).size == 0) &&
(changes == null || ((!) changes).size == 0))
{
/* Don't bother emitting it if nothing's changed */
return;
}
_added = (added != null) ? (!) added : new HashSet ();
_removed = (removed != null) ? (!) removed : new HashSet ();
if (changes != null)
{
_changes = (!) changes;
}
else
{
_changes = new HashMultiMap ();
}
/* Debug output. */
if (this._debug.debug_output_enabled == true)
{
debug ("Emitting individuals-changed-detailed with %u mappings:",
_changes.size);
foreach (var removed_ind in _changes.get_keys ())
{
foreach (var added_ind in _changes.get (removed_ind))
{
debug (" %s (%p) → %s (%p)",
(removed_ind != null) ? ((!) removed_ind).id : "",
removed_ind,
(added_ind != null) ? ((!) added_ind).id : "", added_ind);
if (removed_ind != null)
{
debug (" Removed individual's personas:");
foreach (var p in ((!) removed_ind).personas)
{
debug (" %s (%p)", p.uid, p);
}
}
if (added_ind != null)
{
debug (" Added individual's personas:");
foreach (var p in ((!) added_ind).personas)
{
debug (" %s (%p)", p.uid, p);
}
}
}
}
}
this.individuals_changed (_added.read_only_view, _removed.read_only_view,
message, actor, reason);
this.individuals_changed_detailed (_changes);
}
private void _connect_to_individual (Individual individual)
{
individual.removed.connect (this._individual_removed_cb);
this._individuals.set (individual.id, individual);
}
private void _disconnect_from_individual (Individual individual)
{
this._individuals.unset (individual.id);
individual.removed.disconnect (this._individual_removed_cb);
}
private void _add_personas (Set added, ref Individual? user,
ref HashMultiMap individuals_changes)
{
foreach (var persona in added)
{
PersonaStoreTrust trust_level = persona.store.trust_level;
/* These are the Individuals whose Personas will be linked together
* to form the `final_individual`.
* Since a given Persona can only be part of one Individual, and the
* code in Persona._set_personas() ensures that there are no duplicate
* Personas in a given Individual, ensuring that there are no
* duplicate Individuals in `candidate_inds` (by using a
* HashSet) guarantees that there will be no duplicate Personas
* in the `final_individual`. */
HashSet candidate_inds = new HashSet ();
var final_personas = new HashSet ();
debug ("Aggregating persona '%s' on '%s'.", persona.uid, persona.iid);
/* If the Persona is the user, we *always* want to link it to the
* existing this.user. */
if (persona.is_user == true && user != null)
{
debug (" Found candidate individual '%s' as user.",
((!) user).id);
candidate_inds.add ((!) user);
}
/* If we don't trust the PersonaStore at all, we can't link the
* Persona to any existing Individual */
if (trust_level != PersonaStoreTrust.NONE)
{
Individual? candidate_ind = this._link_map.lookup (persona.iid);
if (candidate_ind != null &&
((!) candidate_ind).trust_level != TrustLevel.NONE &&
!candidate_inds.contains ((!) candidate_ind))
{
debug (" Found candidate individual '%s' by IID '%s'.",
((!) candidate_ind).id, persona.iid);
candidate_inds.add ((!) candidate_ind);
}
}
if (persona.store.trust_level == PersonaStoreTrust.FULL)
{
/* If we trust the PersonaStore the Persona came from, we can
* attempt to link based on its linkable properties. */
foreach (unowned string foo in persona.linkable_properties)
{
/* FIXME: If we just use string prop_name directly in the
* foreach, Vala doesn't copy it into the closure data, and
* prop_name ends up as NULL. bgo#628336 */
unowned string prop_name = foo;
/* FIXME: can't be var because of bgo#638208 */
unowned ObjectClass pclass = persona.get_class ();
if (pclass.find_property (prop_name) == null)
{
warning (
/* Translators: the parameter is a property name. */
_("Unknown property '%s' in linkable property list."),
prop_name);
continue;
}
persona.linkable_property_to_links (prop_name, (l) =>
{
unowned string prop_linking_value = l;
Individual? candidate_ind =
this._link_map.lookup (prop_linking_value);
if (candidate_ind != null &&
((!) candidate_ind).trust_level != TrustLevel.NONE &&
!candidate_inds.contains ((!) candidate_ind))
{
debug (" Found candidate individual '%s' by " +
"linkable property '%s' = '%s'.",
((!) candidate_ind).id, prop_name,
prop_linking_value);
candidate_inds.add ((!) candidate_ind);
}
});
}
}
/* Ensure the original persona makes it into the final individual */
final_personas.add (persona);
if (candidate_inds.size > 0 && this._linking_enabled == true)
{
/* The Persona's IID or linkable properties match one or more
* linkable fields which are already in the link map, so we link
* together all the Individuals we found to form a new
* final_individual. Later, we remove the Personas from the old
* Individuals so that the Individuals themselves are removed. */
foreach (var individual in candidate_inds)
{
final_personas.add_all (individual.personas);
}
}
else if (candidate_inds.size > 0)
{
debug (" Linking disabled.");
}
else
{
debug (" Did not find any candidate individuals.");
}
/* Create the final linked Individual */
var final_individual = new Individual (final_personas);
debug (" Created new individual '%s' (%p) with personas:",
final_individual.id, final_individual);
foreach (var p in final_personas)
{
debug (" %s (%p)", p.uid, p);
this._add_persona_to_link_map (p, final_individual);
}
uint num_mappings_added = 0;
foreach (var i in candidate_inds)
{
/* Transitively update the individuals_changes. We have to do this
* in two stages as we can't modify individuals_changes while
* iterating over it. */
var transitive_updates = new HashSet ();
foreach (var k in individuals_changes.get_keys ())
{
if (i in individuals_changes.get (k))
{
transitive_updates.add (k);
}
}
foreach (var k in transitive_updates)
{
assert (individuals_changes.remove (k, i) == true);
/* If we're saying the final_individual is replacing some of
* these candidate individuals, we don't also want to say that
* it's been added (by also emitting a mapping from
* null → final_individual). */
if (k != null)
{
individuals_changes.set (k, final_individual);
num_mappings_added++;
}
}
/* If there were no transitive changes to make, it's because this
* candidate individual existed before this call to
* _add_personas(), so it's safe to say it's being replaced by
* the final_individual. */
if (transitive_updates.size == 0)
{
individuals_changes.set (i, final_individual);
num_mappings_added++;
}
}
/* If there were no candidate individuals or they were all freshly
* added (i.e. mapped from null → candidate_individual), mark the
* final_individual as added. */
if (num_mappings_added == 0)
{
individuals_changes.set (null, final_individual);
}
/* If the final Individual is the user, set them as such. */
if (final_individual.is_user == true)
user = final_individual;
}
}
private void _persona_linkable_property_changed_cb (Object obj,
ParamSpec pspec)
{
/* The value of one of the linkable properties of one the personas has
* changed, so that persona might require re-linking. We do this in a
* simplistic and hacky way (which should work) by simply treating the
* persona as if it's been removed and re-added. */
var persona = (!) (obj as Persona);
debug ("Linkable property '%s' changed for persona '%s' " +
"(is user: %s, IID: %s).", pspec.name, persona.uid,
persona.is_user ? "yes" : "no", persona.iid);
var persona_set = new HashSet ();
persona_set.add (persona);
this._personas_changed_cb (persona.store, persona_set, persona_set,
null, null, GroupDetails.ChangeReason.NONE);
}
private void _connect_to_persona (Persona persona)
{
foreach (var prop_name in persona.linkable_properties)
{
persona.notify[prop_name].connect (
this._persona_linkable_property_changed_cb);
}
}
private void _disconnect_from_persona (Persona persona)
{
foreach (var prop_name in persona.linkable_properties)
{
persona.notify[prop_name].disconnect (
this._persona_linkable_property_changed_cb);
}
}
private void _add_persona_to_link_map (Persona persona, Individual individual)
{
debug ("Connecting to Persona: %s (is user: %s, IID: %s)", persona.uid,
persona.is_user ? "yes" : "no", persona.iid);
debug (" Mapping to Individual: %s", individual.id);
/* Add the Persona to the link map. Its trust level will be reflected in
* final_individual.trust_level, so other Personas won't be linked against
* it in error if the trust level is NONE. */
this._link_map.replace (persona.iid, individual);
/* Only allow linking on non-IID properties of the Persona if we fully
* trust the PersonaStore it came from. */
if (persona.store.trust_level == PersonaStoreTrust.FULL)
{
debug (" Inserting links:");
/* Insert maps from the Persona's linkable properties to the
* Individual. */
foreach (unowned string prop_name in persona.linkable_properties)
{
debug (" %s", prop_name);
/* FIXME: can't be var because of bgo#638208 */
unowned ObjectClass pclass = persona.get_class ();
if (pclass.find_property (prop_name) == null)
{
warning (
/* Translators: the parameter is a property name. */
_("Unknown property '%s' in linkable property list."),
prop_name);
continue;
}
persona.linkable_property_to_links (prop_name, (l) =>
{
unowned string prop_linking_value = l;
debug (" %s", prop_linking_value);
this._link_map.replace (prop_linking_value, individual);
});
}
}
}
/* We remove individuals as a whole from the link map, rather than iterating
* through the link map keys generated by their personas (as in
* _add_persona_to_link_map()) because the values of the personas' linkable
* properties may well have changed since we added the personas to the link
* map. If that's the case, we don't want to end up leaving stale entries in
* the link map, since that *will* cause problems later on. */
private void _remove_individual_from_link_map (Individual individual)
{
debug ("Removing Individual '%s' from the link map.", individual.id);
var iter = HashTableIter (this._link_map);
string link_key;
Individual link_individual;
while (iter.next (out link_key, out link_individual) == true)
{
if (link_individual == individual)
{
debug (" %s → %s (%p)",
link_key, link_individual.id, link_individual);
iter.remove ();
}
}
}
private void _personas_changed_cb (PersonaStore store,
Set added,
Set removed,
string? message,
Persona? actor,
GroupDetails.ChangeReason reason)
{
var removed_individuals = new HashSet ();
var individuals_changes = new HashMultiMap ();
var relinked_personas = new HashSet ();
var replaced_individuals = new HashMap ();
/* We store the value of this.user locally and only update it at the end
* of the function to prevent spamming notifications of changes to the
* property. */
var user = this.user;
debug ("Removing Personas:");
foreach (var persona in removed)
{
debug (" %s (is user: %s, IID: %s)", persona.uid,
persona.is_user ? "yes" : "no", persona.iid);
/* Find the Individual containing the Persona (if any) and mark them
* for removal (any other Personas they have which aren't being
* removed will be re-linked into other Individuals). */
Individual? ind = this._link_map.lookup (persona.iid);
if (ind != null)
{
removed_individuals.add ((!) ind);
}
/* Stop listening to notifications about the persona's linkable
* properties. */
this._disconnect_from_persona (persona);
}
/* Remove the Individuals which were pointed to by the linkable properties
* of the removed Personas. We can then re-link the other Personas in
* those Individuals, since their links may have changed.
* Note that we remove the Individual from this.individuals, meaning that
* _individual_removed_cb() ignores this Individual. This allows us to
* group together the IndividualAggregator.individuals_changed signals
* for all the removed Individuals. */
debug ("Removing Individuals due to removed links:");
foreach (var individual in removed_individuals)
{
/* Ensure we don't remove the same Individual twice */
if (this._individuals.has_key (individual.id) == false)
continue;
debug (" %s", individual.id);
/* Build a list of Personas which need relinking. Ensure we don't
* include any of the Personas which have just been removed. */
foreach (var persona in individual.personas)
{
if (removed.contains (persona) == true ||
relinked_personas.contains (persona) == true)
continue;
relinked_personas.add (persona);
}
if (user == individual)
user = null;
this._disconnect_from_individual (individual);
/* Remove the Individual's links from the link map */
this._remove_individual_from_link_map (individual);
}
debug ("Adding Personas:");
foreach (var persona in added)
{
debug (" %s (is user: %s, IID: %s)", persona.uid,
persona.is_user ? "yes" : "no", persona.iid);
/* Connect to notifications about the persona's linkable
* properties. */
this._connect_to_persona (persona);
}
if (added.size > 0)
{
this._add_personas (added, ref user, ref individuals_changes);
}
debug ("Relinking Personas:");
foreach (var persona in relinked_personas)
{
debug (" %s (is user: %s, IID: %s)", persona.uid,
persona.is_user ? "yes" : "no", persona.iid);
}
this._add_personas (relinked_personas, ref user, ref individuals_changes);
/* Work out which final individuals have replaced the removed_individuals
* and update individuals_changes accordingly. */
foreach (var individual in removed_individuals)
{
var added_mapping = false;
foreach (var persona in individual.personas)
{
if (!(persona in removed) || (persona in added))
{
individuals_changes.remove (null, persona.individual);
individuals_changes.set (individual, persona.individual);
added_mapping = true;
}
}
/* Has the individual been removed entirely? */
if (added_mapping == false)
{
individuals_changes.set (individual, null);
}
individual.personas = null;
}
/* Notify of changes to this.user */
this.user = user;
/* Signal the addition of new individuals and removal of old ones to the
* aggregator */
if (individuals_changes.size > 0)
{
var added_individuals = new HashSet ();
/* Extract the deprecated added and removed sets from
* individuals_changes, to be used in the individuals_changed
* signal. */
foreach (var old_ind in individuals_changes.get_keys ())
{
foreach (var new_ind in individuals_changes.get (old_ind))
{
assert (old_ind != null || new_ind != null);
if (old_ind != null)
{
removed_individuals.add ((!) old_ind);
}
if (new_ind != null)
{
added_individuals.add ((!) new_ind);
this._connect_to_individual ((!) new_ind);
}
if (old_ind != null && new_ind != null)
{
replaced_individuals.set ((!) old_ind, (!) new_ind);
}
}
}
this._emit_individuals_changed (added_individuals,
removed_individuals, individuals_changes);
}
/* Signal the replacement of various Individuals as a consequence of
* linking. */
debug ("Replacing Individuals due to linking:");
var iter = replaced_individuals.map_iterator ();
while (iter.next () == true)
{
var old_ind = iter.get_key ();
var new_ind = iter.get_value ();
debug (" %s (%p) → %s (%p)", old_ind.id, old_ind,
new_ind.id, new_ind);
old_ind.replace (new_ind);
}
/* Validate the link map. */
if (this._debug.debug_output_enabled == true)
{
var iter2 = HashTableIter (this._link_map);
string link_key;
Individual individual;
while (iter2.next (out link_key, out individual) == true)
{
if (this._individuals.get (individual.id) != individual)
{
warning ("Link map contains invalid mapping:\n" +
" %s → %s (%p)",
link_key, individual.id, individual);
warning ("Individual %s (%p) personas:", individual.id,
individual);
foreach (var p in individual.personas)
{
warning (" %s (%p)", p.uid, p);
}
}
}
}
}
private void _is_primary_store_changed_cb (Object object, ParamSpec pspec)
{
/* Ensure that we only have one primary PersonaStore */
var store = (PersonaStore) object;
assert ((store.is_primary_store == true &&
store == this._primary_store) ||
(store.is_primary_store == false &&
store != this._primary_store));
}
private void _persona_store_is_quiescent_changed_cb (Object obj,
ParamSpec pspec)
{
/* Have we reached a quiescent state yet? */
if (this._non_quiescent_persona_store_count > 0)
{
this._non_quiescent_persona_store_count--;
this._notify_if_is_quiescent ();
}
}
private void _backend_is_quiescent_changed_cb (Object obj, ParamSpec pspec)
{
if (this._non_quiescent_backend_count > 0)
{
this._non_quiescent_backend_count--;
this._notify_if_is_quiescent ();
}
}
private void _notify_if_is_quiescent ()
{
if (this._non_quiescent_backend_count == 0 &&
this._non_quiescent_persona_store_count == 0 &&
this._is_quiescent == false)
{
if (this._configured_primary_store_type_id.length > 0 &&
this._primary_store == null)
{
warning ("Failed to find primary PersonaStore with type ID " +
"'%s' and ID '%s'.\n" +
"Individuals will not be linked properly " +
"and creating new links between Personas will not work.\n" +
"The configured primary PersonaStore's backend may not be " +
"installed. If you are unsure, check with your " +
"distribution.",
this._configured_primary_store_type_id,
this._configured_primary_store_id);
}
this._is_quiescent = true;
this.notify_property ("is-quiescent");
}
}
private void _persona_store_is_user_set_default_changed_cb (Object obj,
ParamSpec pspec)
{
var store = (PersonaStore) obj;
debug ("PersonaStore.is-user-set-default changed for store %p " +
"(type ID: %s, ID: %s)", store, store.type_id, store.id);
if (this._maybe_configure_as_primary (store))
this._set_primary_store (store);
}
private bool _maybe_configure_as_primary (PersonaStore store)
{
debug ("_maybe_configure_as_primary()");
var configured = false;
if (!this._user_configured_primary_store &&
store.is_user_set_default)
{
debug ("Setting primary store IDs to '%s' and '%s'.", store.type_id,
store.id);
this._configured_primary_store_type_id = store.type_id;
this._configured_primary_store_id = store.id;
configured = true;
}
return configured;
}
private void _individual_removed_cb (Individual i, Individual? replacement)
{
if (this.user == i)
this.user = null;
/* Only signal if the individual is still in this.individuals. This allows
* us to group removals together in, e.g., _personas_changed_cb(). */
if (this._individuals.get (i.id) != i)
return;
if (replacement != null)
{
debug ("Individual '%s' removed (replaced by '%s')", i.id,
((!) replacement).id);
}
else
{
debug ("Individual '%s' removed (not replaced)", i.id);
}
/* If the individual has 0 personas, we've already signaled removal */
if (i.personas.size > 0)
{
var changes = new HashMultiMap ();
var individuals = new HashSet ();
individuals.add (i);
changes.set (i, replacement);
this._emit_individuals_changed (null, individuals, changes);
}
this._disconnect_from_individual (i);
}
/**
* Add a new persona in the given {@link PersonaStore} based on the `details`
* provided.
*
* If the target store is offline, this function will throw
* {@link IndividualAggregatorError.STORE_OFFLINE}. It's the responsibility of
* the caller to cache details and re-try this function if it wishes to make
* offline adds work.
*
* The details hash is a backend-specific mapping of key, value strings.
* Common keys include:
*
* * contact - service-specific contact ID
* * message - a user-readable message to pass to the persona being added
*
* If a {@link Persona} with the given details already exists in the store, no
* error will be thrown and this function will return `null`.
*
* @param parent an optional {@link Individual} to add the new {@link Persona}
* to. This persona will be appended to its ordered list of personas.
* @param persona_store the {@link PersonaStore} to add the persona to
* @param details a key-value map of details to use in creating the new
* {@link Persona}
* @return the new {@link Persona} or `null` if the corresponding
* {@link Persona} already existed. If non-`null`, the new {@link Persona}
* will also be added to a new or existing {@link Individual} as necessary.
*
* @since 0.3.5
*/
public async Persona? add_persona_from_details (Individual? parent,
PersonaStore persona_store,
HashTable details) throws IndividualAggregatorError
{
Persona? persona = null;
try
{
var details_copy = this._asv_copy (details);
persona = yield persona_store.add_persona_from_details (details_copy);
}
catch (PersonaStoreError e)
{
if (e is PersonaStoreError.STORE_OFFLINE)
{
throw new IndividualAggregatorError.STORE_OFFLINE (e.message);
}
else
{
var full_id = this._get_store_full_id (persona_store.type_id,
persona_store.id);
throw new IndividualAggregatorError.ADD_FAILED (
/* Translators: the first parameter is a store identifier
* and the second parameter is an error message. */
_("Failed to add contact for persona store ID '%s': %s"),
full_id, e.message);
}
}
if (parent != null && persona != null)
{
((!) parent).personas.add ((!) persona);
}
return persona;
}
private HashTable _asv_copy (HashTable asv)
{
var retval = new HashTable (str_hash, str_equal);
asv.foreach ((k, v) =>
{
retval.insert ((string) k, v);
});
return retval;
}
/**
* Completely remove the individual and all of its personas from their
* backing stores.
*
* @param individual the {@link Individual} to remove
* @since 0.1.11
*/
public async void remove_individual (Individual individual) throws GLib.Error
{
/* Removing personas changes the persona set so we need to make a copy
* first */
var personas = new HashSet ();
foreach (var p in individual.personas)
{
personas.add (p);
}
foreach (var persona in personas)
{
yield persona.store.remove_persona (persona);
}
}
/**
* Completely remove the persona from its backing store.
*
* This will leave other personas in the same individual alone.
*
* @param persona the {@link Persona} to remove
* @since 0.1.11
*/
public async void remove_persona (Persona persona) throws GLib.Error
{
yield persona.store.remove_persona (persona);
}
/**
* Link the given {@link Persona}s together.
*
* Create links between the given {@link Persona}s so that they form a single
* {@link Individual}. The new {@link Individual} will be returned via the
* {@link IndividualAggregator.individuals_changed} signal.
*
* Removal of the {@link Individual}s which the {@link Persona}s were in
* before is signalled by {@link IndividualAggregator.individuals_changed} and
* {@link Individual.removed}.
*
* @param personas the {@link Persona}s to be linked
* @since 0.5.1
*/
public async void link_personas (Set personas)
throws IndividualAggregatorError
{
if (this._primary_store == null)
{
throw new IndividualAggregatorError.NO_PRIMARY_STORE (
_("Can’t link personas with no primary store.") + "\n" +
_("Persona store ‘%s:%s’ is configured as primary, but could not be found or failed to load.") + "\n" +
_("Check the service providing the persona store is running, or change the default store in that service or using the “%s” GConf key."),
this._configured_primary_store_type_id,
this._configured_primary_store_id, this._FOLKS_CONFIG_KEY);
}
/* Don't bother linking if it's just one Persona */
if (personas.size <= 1)
return;
/* Disallow linking if it's disabled */
if (this._linking_enabled == false)
{
debug ("Can't link Personas: linking disabled.");
return;
}
/* Create a new persona in the primary store which links together the
* given personas */
assert (((!) this._primary_store).type_id ==
this._configured_primary_store_type_id);
/* `protocols_addrs_set` will be passed to the new Kf.Persona */
var protocols_addrs_set = new HashMultiMap (
null, null,
(GLib.HashFunc) ImFieldDetails.hash,
(GLib.EqualFunc) ImFieldDetails.equal);
var web_service_addrs_set =
new HashMultiMap (
null, null,
(GLib.HashFunc) WebServiceFieldDetails.hash,
(GLib.EqualFunc) WebServiceFieldDetails.equal);
/* List of local_ids */
var local_ids = new Gee.HashSet ();
foreach (var persona in personas)
{
if (persona is ImDetails)
{
ImDetails im_details = (ImDetails) persona;
/* protocols_addrs_set = union (all personas' IM addresses) */
foreach (var protocol in im_details.im_addresses.get_keys ())
{
var im_addresses = im_details.im_addresses.get (protocol);
foreach (var im_address in im_addresses)
{
protocols_addrs_set.set (protocol, im_address);
}
}
}
if (persona is WebServiceDetails)
{
WebServiceDetails ws_details = (WebServiceDetails) persona;
/* web_service_addrs_set = union (all personas' WS addresses) */
foreach (var web_service in
ws_details.web_service_addresses.get_keys ())
{
var ws_addresses =
ws_details.web_service_addresses.get (web_service);
foreach (var ws_fd in ws_addresses)
web_service_addrs_set.set (web_service, ws_fd);
}
}
if (persona is LocalIdDetails)
{
foreach (var id in ((LocalIdDetails) persona).local_ids)
{
local_ids.add (id);
}
}
}
var details = new HashTable (str_hash, str_equal);
if (protocols_addrs_set.size > 0)
{
var im_addresses_value = Value (typeof (MultiMap));
im_addresses_value.set_object (protocols_addrs_set);
details.insert (
(!) PersonaStore.detail_key (PersonaDetail.IM_ADDRESSES),
im_addresses_value);
}
if (web_service_addrs_set.size > 0)
{
var web_service_addresses_value = Value (typeof (MultiMap));
web_service_addresses_value.set_object (web_service_addrs_set);
details.insert (
(!) PersonaStore.detail_key (PersonaDetail.WEB_SERVICE_ADDRESSES),
web_service_addresses_value);
}
if (local_ids.size > 0)
{
var local_ids_value = Value (typeof (Set));
local_ids_value.set_object (local_ids);
details.insert (
(!) Folks.PersonaStore.detail_key (PersonaDetail.LOCAL_IDS),
local_ids_value);
}
yield this.add_persona_from_details (null,
(!) this._primary_store, details);
}
/**
* Unlinks the given {@link Individual} into its constituent {@link Persona}s.
*
* This completely unlinks the given {@link Individual}, destroying all of
* its writeable {@link Persona}s.
*
* The {@link Individual}'s removal is signalled by
* {@link IndividualAggregator.individuals_changed} and
* {@link Individual.removed}.
*
* The {@link Persona}s comprising the {@link Individual} will be re-linked
* into one or more new {@link Individual}s, depending on how much linking
* data remains (typically only implicit links remain). The addition of these
* new {@link Individual}s will be signalled by
* {@link IndividualAggregator.individuals_changed}.
*
* @param individual the {@link Individual} to unlink
* @since 0.1.13
*/
public async void unlink_individual (Individual individual) throws GLib.Error
{
if (this._linking_enabled == false)
{
debug ("Can't unlink Individual '%s': linking disabled.",
individual.id);
return;
}
debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
/* Remove all the Personas from writeable PersonaStores.
*
* We have to take a copy of the Persona list before removing the
* Personas, as _personas_changed_cb() (which is called as a result of
* calling _primary_store.remove_persona()) messes around with Persona
* lists. */
var personas = new HashSet ();
foreach (var p in individual.personas)
{
personas.add (p);
}
foreach (var persona in personas)
{
/* Since persona.store != null, we know that
* this._primary_store != null. */
if (persona.store == this._primary_store)
{
debug (" %s (is user: %s, IID: %s)", persona.uid,
persona.is_user ? "yes" : "no", persona.iid);
yield ((!) this._primary_store).remove_persona (persona);
}
}
}
/**
* Ensure that the given property is writeable for the given
* {@link Individual}.
*
* This makes sure that there is at least one {@link Persona} in the
* individual which has `property_name` in its
* {@link Persona.writeable_properties}. If no such persona exists in the
* individual, a new one will be created and linked to the individual. (Note
* that due to the design of the aggregator, this will result in the previous
* individual being removed and replaced by a new one with the new persona;
* listen to the {@link Individual.removed} signal to see the replacement.)
*
* It may not be possible to create a new persona which has the given property
* as writeable. In that case, a
* {@link IndividualAggregatorError.NO_PRIMARY_STORE} or
* {@link IndividualAggregatorError.PROPERTY_NOT_WRITEABLE} error will be
* thrown.
*
* @param individual the individual for which `property_name` should be
* writeable
* @param property_name the name of the property which needs to be writeable
* (this should be in lower case using hyphens, e.g. “web-service-addresses”)
* @return a persona (new or existing) which has the given property as
* writeable
*
* @since 0.6.2
*/
public async Persona ensure_individual_property_writeable (
Individual individual, string property_name)
throws IndividualAggregatorError
{
debug ("ensure_individual_property_writeable: %s, %s",
individual.id, property_name);
/* See if the individual already contains the property we want. */
foreach (var p1 in individual.personas)
{
if (property_name in p1.writeable_properties)
{
debug (" Returning existing persona: %s", p1.uid);
return p1;
}
}
/* Otherwise, create a new persona in the writeable store. If the
* writeable store doesn't exist or doesn't support writing to the given
* property, we try the other persona stores. */
var details = new HashTable (str_hash, str_equal);
Persona? new_persona = null;
if (this._primary_store != null &&
property_name in
((!) this._primary_store).always_writeable_properties)
{
try
{
debug (" Using writeable store");
new_persona = yield this.add_persona_from_details (null,
(!) this._primary_store, details);
}
catch (IndividualAggregatorError e1)
{
/* Ignore it */
new_persona = null;
}
}
if (new_persona == null)
{
foreach (var s in this._stores.values)
{
if (s == this._primary_store ||
!(property_name in s.always_writeable_properties))
{
/* Skip the store we've just tried */
continue;
}
try
{
debug (" Using store %s", s.id);
new_persona = yield this.add_persona_from_details (null, s,
details);
}
catch (IndividualAggregatorError e2)
{
/* Ignore it */
new_persona = null;
continue;
}
}
}
/* Throw an error if we haven't managed to find a suitable store */
if (new_persona == null && this._primary_store == null)
{
throw new IndividualAggregatorError.NO_PRIMARY_STORE (
_("Can’t add personas with no primary store.") + "\n" +
_("Persona store ‘%s:%s’ is configured as primary, but could not be found or failed to load.") + "\n" +
_("Check the service providing the persona store is running, or change the default store in that service or using the “%s” GConf key."),
this._configured_primary_store_type_id,
this._configured_primary_store_id, this._FOLKS_CONFIG_KEY);
}
else if (new_persona == null)
{
throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
_("Can't write to requested property (“%s”) of the writeable store."),
property_name);
}
/* Link the persona to the existing individual. We can guarantee
* new_persona != null because we'd have bailed out above otherwise. */
var linking_personas = new HashSet ();
linking_personas.add ((!) new_persona);
foreach (var p2 in individual.personas)
{
linking_personas.add (p2);
}
debug (" Linking personas to ensure %s property is writeable.",
property_name);
yield this.link_personas (linking_personas);
return (!) new_persona;
}
}