diff options
author | Alexander Larsson <alexl@redhat.com> | 2011-09-08 18:33:03 +0200 |
---|---|---|
committer | Alexander Larsson <alexl@redhat.com> | 2011-09-09 14:18:04 +0200 |
commit | 44e73d7851a0803a43acadbb1e3c816d1dbf0a22 (patch) | |
tree | 83c21429f5123bd600a89c34f9ea33b9c1b6c13a /src | |
parent | a5be4423cc1118fce29fe8a994334e930588ca23 (diff) | |
download | gnome-contacts-44e73d7851a0803a43acadbb1e3c816d1dbf0a22.tar.gz |
Initial cut at unlinking algorithm
Diffstat (limited to 'src')
-rw-r--r-- | src/contacts-linking.vala | 473 |
1 files changed, 473 insertions, 0 deletions
diff --git a/src/contacts-linking.vala b/src/contacts-linking.vala index d2418ec..95663c0 100644 --- a/src/contacts-linking.vala +++ b/src/contacts-linking.vala @@ -265,4 +265,477 @@ namespace Contacts { yield link_data.apply_to_persona (writable_persona); } } + + internal abstract class PersonaAttribute : Object { + public string property_name; + + public static HashSet<PersonaAttribute> create_set () { + return new HashSet<PersonaAttribute>((GLib.HashFunc) PersonaAttribute.hash, + (GLib.EqualFunc) PersonaAttribute.equal); + } + + public virtual bool is_removable (Persona from_persona) { + return (property_name in from_persona.writeable_properties); + } + + public abstract bool is_referenced_by_persona (Persona persona); + + public abstract string to_string (); + + public virtual bool equal (PersonaAttribute that) { + return this.property_name == that.property_name; + } + + public virtual uint hash () { + return this.property_name.hash (); + } + + public abstract async void persona_apply_attributes (Persona persona, + Set<PersonaAttribute> added_attributes, + Set<PersonaAttribute> removed_attributes); + } + + internal class PersonaAttributeLocalId : PersonaAttribute { + string value; + + public PersonaAttributeLocalId (string value) { + property_name = "local-ids"; + this.value = value; + } + + public override bool is_removable (Persona from_persona) { + return base.is_removable (from_persona) && value != from_persona.iid; + } + + public override string to_string () { + return "local_id: " + value; + } + + public override bool is_referenced_by_persona (Persona persona) { + var details = persona as LocalIdDetails; + if (details == null) + return false; + + return value in details.local_ids; + } + + public override async void persona_apply_attributes (Persona persona, + Set<PersonaAttribute> added_attributes, + Set<PersonaAttribute> removed_attributes) { + var details = persona as LocalIdDetails; + if (details == null) + return; + + var added_values = new HashSet<string> (); + foreach (var added in added_attributes) { + added_values.add (((PersonaAttributeLocalId)added).value); + } + + var removed_values = new HashSet<string> (); + foreach (var removed in removed_attributes) { + removed_values.add (((PersonaAttributeLocalId)removed).value); + } + + var new_values = new HashSet<string> (); + bool changed = false; + foreach (var v in details.local_ids) { + if (v in removed_values) { + changed = true; + continue; + } + new_values.add (v); + if (v in added_values) + added_values.remove (v); + } + foreach (var v2 in added_values) { + changed = true; + new_values.add (v2); + } + + if (changed) { + try { + yield details.change_local_ids (new_values); + } catch (GLib.Error e) { + warning ("Unable to set local ids when linking: %s\n", e.message); + } + } + } + + public override bool equal (PersonaAttribute _that) { + var that = _that as PersonaAttributeLocalId; + return + that != null && + base.equal (that) && + this.value == that.value; + } + + public override uint hash () { + return this.value.hash () ^ base.hash (); + } + } + + internal class PersonaAttributeImAddress : PersonaAttribute { + string protocol; + ImFieldDetails detail; + + public PersonaAttributeImAddress (string protocol, ImFieldDetails detail) { + property_name = "im-addresses"; + this.protocol = protocol; + this.detail = detail; + } + + public override string to_string () { + return "im_addresses: " + protocol + ":" + detail.value; + } + + public override bool is_referenced_by_persona (Persona persona) { + var details = persona as ImDetails; + if (details == null) + return false; + + return detail in details.im_addresses.get (protocol); + } + + public override async void persona_apply_attributes (Persona persona, + Set<PersonaAttribute> added_attributes, + Set<PersonaAttribute> removed_attributes) { + var details = persona as ImDetails; + if (details == null) + return; + + var added_values = new HashMultiMap<string, ImFieldDetails> (null, null, + (GLib.HashFunc) ImFieldDetails.hash, + (GLib.EqualFunc) ImFieldDetails.equal); + foreach (var added in added_attributes) { + added_values.set (((PersonaAttributeImAddress)added).protocol, ((PersonaAttributeImAddress)added).detail); + } + + var removed_values = new HashMultiMap<string, ImFieldDetails> (null, null, + (GLib.HashFunc) ImFieldDetails.hash, + (GLib.EqualFunc) ImFieldDetails.equal); + foreach (var removed in removed_attributes) { + removed_values.set (((PersonaAttributeImAddress)removed).protocol, ((PersonaAttributeImAddress)removed).detail); + } + + var new_values = + new HashMultiMap<string, ImFieldDetails> (null, null, + (GLib.HashFunc) ImFieldDetails.hash, + (GLib.EqualFunc) ImFieldDetails.equal); + bool changed = false; + foreach (var proto1 in details.im_addresses.get_keys ()) { + foreach (var detail1 in details.im_addresses.get (proto1)) { + if (removed_values.get (proto1).contains (detail1)) { + changed = true; + continue; + } + new_values.set (proto1, detail1); + if (added_values.get (proto1).contains (detail1)) { + added_values.remove (proto1, detail1); + } + } + } + foreach (var proto2 in added_values.get_keys ()) { + foreach (var detail2 in added_values.get (proto2)) { + changed = true; + new_values.set (proto2, detail2); + } + } + + if (changed) { + try { + yield details.change_im_addresses (new_values); + } catch (GLib.Error e) { + warning ("Unable to set im address when linking: %s\n", e.message); + } + } + } + + public override bool equal (PersonaAttribute _that) { + var that = _that as PersonaAttributeImAddress; + return + that != null && + base.equal (that) && + this.protocol == that.protocol && + this.detail.equal (that.detail); + } + + public override uint hash () { + return this.protocol.hash () ^ this.detail.hash () ^ base.hash (); + } + } + + internal class PersonaAttributeWebService : PersonaAttribute { + string service; + WebServiceFieldDetails detail; + + public PersonaAttributeWebService (string service, WebServiceFieldDetails detail) { + property_name = "web-service-addresses"; + this.service = service; + this.detail = detail; + } + + public override string to_string () { + return "web_service_addresses: " + service + ":" + detail.value; + } + + public override bool is_referenced_by_persona (Persona persona) { + var details = persona as WebServiceDetails; + if (details == null) + return false; + + return detail in details.web_service_addresses.get (service); + } + + public override async void persona_apply_attributes (Persona persona, + Set<PersonaAttribute> added_attributes, + Set<PersonaAttribute> removed_attributes) { + var details = persona as WebServiceDetails; + if (details == null) + return; + + var added_values = new HashMultiMap<string, WebServiceFieldDetails> (null, null, + (GLib.HashFunc) WebServiceFieldDetails.hash, + (GLib.EqualFunc) WebServiceFieldDetails.equal); + foreach (var added in added_attributes) { + added_values.set (((PersonaAttributeWebService)added).service, ((PersonaAttributeWebService)added).detail); + } + + var removed_values = new HashMultiMap<string, WebServiceFieldDetails> (null, null, + (GLib.HashFunc) WebServiceFieldDetails.hash, + (GLib.EqualFunc) WebServiceFieldDetails.equal); + foreach (var removed in removed_attributes) { + removed_values.set (((PersonaAttributeWebService)removed).service, ((PersonaAttributeWebService)removed).detail); + } + + var new_values = + new HashMultiMap<string, WebServiceFieldDetails> (null, null, + (GLib.HashFunc) WebServiceFieldDetails.hash, + (GLib.EqualFunc) WebServiceFieldDetails.equal); + bool changed = false; + foreach (var srv1 in details.web_service_addresses.get_keys ()) { + foreach (var detail1 in details.web_service_addresses.get (srv1)) { + if (removed_values.get (srv1).contains (detail1)) { + changed = true; + continue; + } + new_values.set (srv1, detail1); + if (added_values.get (srv1).contains (detail1)) { + added_values.remove (srv1, detail1); + } + } + } + foreach (var srv2 in added_values.get_keys ()) { + foreach (var detail2 in added_values.get (srv2)) { + changed = true; + new_values.set (srv2, detail2); + } + } + + if (changed) { + try { + yield details.change_web_service_addresses (new_values); + } catch (GLib.Error e) { + warning ("Unable to set web service when linking: %s\n", e.message); + } + } + } + + public override bool equal (PersonaAttribute _that) { + var that = _that as PersonaAttributeWebService; + return + that != null && + base.equal (that) && + this.service == that.service && + this.detail.equal (that.detail); + } + + public override uint hash () { + return this.service.hash () ^ this.detail.hash () ^ base.hash (); + } + } + + public static Set<PersonaAttribute> get_linkable_attributes (Persona persona) { + var res = PersonaAttribute.create_set (); + + if (persona is LocalIdDetails) { + foreach (var id in ((LocalIdDetails) persona).local_ids) { + res.add (new PersonaAttributeLocalId (id)); + } + } + + if (persona is ImDetails) { + foreach (var proto in ((ImDetails) persona).im_addresses.get_keys ()) { + foreach (var im in ((ImDetails) persona).im_addresses.get (proto)) { + res.add (new PersonaAttributeImAddress (proto, im)); + } + } + } + + if (persona is WebServiceDetails) { + foreach (var srv in ((WebServiceDetails) persona).web_service_addresses.get_keys ()) { + foreach (var web in ((WebServiceDetails) persona).web_service_addresses.get (srv)) { + res.add (new PersonaAttributeWebService (srv, web)); + } + } + } + + return res; + } + + public static bool persona_can_link_to (Persona persona, Set<PersonaAttribute> attributes) { + var property_names = new HashSet<string>(str_hash, str_equal); + foreach (var a in attributes) + property_names.add (a.property_name); + + foreach (var p in property_names) { + if (! (p in persona.writeable_properties)) + return false; + } + return true; + } + + internal bool attr_type_equal (PersonaAttribute a, PersonaAttribute b) { + return + a.get_type() == b.get_type() && + a.property_name == b.property_name; + } + + internal uint attr_type_hash (PersonaAttribute key) { + return (uint)key.get_type() ^ key.property_name. hash (); + } + + public static async void persona_apply_attributes (Persona persona, + Set<PersonaAttribute>? added_attributes, + Set<PersonaAttribute>? removed_attributes) { + var properties = new HashSet<PersonaAttribute>((GLib.HashFunc)attr_type_hash, (GLib.EqualFunc) attr_type_equal); + + if (added_attributes != null) { + foreach (var a1 in added_attributes) { + properties.add (a1); + } + } + if (removed_attributes != null) { + foreach (var a2 in removed_attributes) { + properties.add (a2); + } + } + + foreach (var property in properties) { + var added = PersonaAttribute.create_set (); + var removed = PersonaAttribute.create_set (); + if (added_attributes != null) { + foreach (var a3 in added_attributes) { + if (attr_type_equal (a3, property)) + added.add (a3); + } + } + if (removed_attributes != null) { + foreach (var a4 in removed_attributes) { + if (attr_type_equal (a4, property)) + removed.add (a4); + } + } + yield property.persona_apply_attributes (persona, added, removed); + } + } + + public async void unlink_persona (Contact contact, Persona persona_to_unlink) { + var individual = contact.individual; + var persona_to_unlink_removals = PersonaAttribute.create_set (); + var other_personas_removals = PersonaAttribute.create_set (); + + foreach (PersonaAttribute a1 in get_linkable_attributes (persona_to_unlink)) { + // Check that this attribute actually is used to link this persona to the individual + bool used_to_link = false; + foreach (var persona in individual.personas) { + if (persona != persona_to_unlink && + a1.is_referenced_by_persona (persona)) { + used_to_link = true; + break; + } + } + if (!used_to_link) + continue; // Wasn't used, no need to do anything about it + + if (a1.is_removable (persona_to_unlink)) { + // We can remove the attribute from the persona, which should completely break any linkage + // due to this attribute + persona_to_unlink_removals.add (a1); + } else { + // We can't remove the attribute from the persona, need to make sure no other persona + // references this + other_personas_removals.add (a1); + } + } + + // At this point we know how to unlink the persona from the individual, however + // doing so may cause the remaining personas to form disjoint sets rather than + // a single Individual. Consider two subsets of personas A and B, and the unlinked + // persona u which make up the original individual. When unlinking u A and B may be + // disjoint if: + // * A links to u and u Links to B, then the data from u that linked it to B was + // removed (and no other links go between A and B) + // or + // * A and B both link to u, but to unlink them from u we removed the data in A and + // B that caused this link (and no other links go between A and B) + // + // To fix this up we need to ensure that all the remaining personas in the inidivudal + // do have links by picking (or creating if there is none) a persona where all linkable + // attributes are writeable and ensuring that it can reach all the other remaining + // personas. We do this the easy way by just adding all linkable attributes to this + // persona + var main_persona_additions = PersonaAttribute.create_set (); + foreach (var p1 in individual.personas) { + if (p1 == persona_to_unlink) + continue; + foreach (PersonaAttribute a2 in get_linkable_attributes (p1)) { + if (a2 in other_personas_removals) + continue; + main_persona_additions.add (a2); + } + } + + // Find tha main persona that will be used to add the extra linking info to + // avoid disjoint sets + Persona? main_persona = null; + foreach (var p2 in individual.personas) { + if (p2 != persona_to_unlink && persona_can_link_to (p2, main_persona_additions)) { + main_persona = p2; + if (main_persona.store.is_writeable) + break; // Exit if we find a primary persona, as we prefer these + } + } + + // We make a copy of the personas as the on in the individual may start + // changing now + var other_personas = new HashSet<Persona>(); + foreach (var p3 in individual.personas) { + if (p3 != persona_to_unlink && + p3 != main_persona) + other_personas.add (p3); + } + + if (main_persona == null) { + var details = new HashTable<string, Value?> (str_hash, str_equal); + try { + main_persona = yield contact.store.aggregator.primary_store.add_persona_from_details (details); + } catch (GLib.Error e) { + warning ("Unable to create new persona when unlinking: %s\n", e.message); + return; + } + } + + persona_to_unlink.set_data ("contacts-new-contact", true); + + // First apply all additions on the primary persona so that we avoid temporarily being + // unlinked and then relinked + // Note, this may cause a new persona to be added to the individual if + // main_persona is null + yield persona_apply_attributes (main_persona, main_persona_additions, other_personas_removals); + foreach (var p in other_personas) { + yield persona_apply_attributes (p, null, other_personas_removals); + } + // Last we do the removals on the persona_to_unlink + yield persona_apply_attributes (persona_to_unlink, null, persona_to_unlink_removals); + } } |