[gnome-contacts] Initial cut at unlinking algorithm



commit 44e73d7851a0803a43acadbb1e3c816d1dbf0a22
Author: Alexander Larsson <alexl redhat com>
Date:   Thu Sep 8 18:33:03 2011 +0200

    Initial cut at unlinking algorithm

 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);
+  }
 }



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]