[folks] core: Add core anti-linking support



commit 1441ae9ddda3dfba8a67d18ecc6a75e16d389553
Author: Philip Withnall <philip tecnocode co uk>
Date:   Mon Jul 25 20:27:29 2011 +0100

    core: Add core anti-linking support
    
    This adds the core of the anti-linking support, based around a new
    AntiLinkable interface. This will be implemented by Persona subclasses which
    can store anti-linking information (in the form of a set of Persona UIDs
    which the given Persona should never be linked to).
    
    This approach allows anti-linking information to be stored with the personas
    (presumably in the primary persona store) and thus it should be network
    transparent. i.e. Using folks on two different computers with a Google
    Contacts address book as primary should cause the anti-linking data to be
    shared.
    
    This also includes the necessary IndividualAggregator changes.
    
    Sadly, no unit tests are included.
    
    Closes: https://bugzilla.gnome.org/show_bug.cgi?id=629537

 NEWS                             |    3 +-
 folks/Makefile.am                |    1 +
 folks/anti-linkable.vala         |  157 ++++++++++++++++++++++++++++++++++++++
 folks/individual-aggregator.vala |  119 ++++++++++++++++++++++++-----
 folks/individual.vala            |   60 ++++++++++++++
 folks/persona-store.vala         |   12 +++-
 folks/potential-match.vala       |   10 ++-
 tools/inspect/utils.vala         |    3 +-
 8 files changed, 340 insertions(+), 25 deletions(-)
---
diff --git a/NEWS b/NEWS
index 336890a..79462fb 100644
--- a/NEWS
+++ b/NEWS
@@ -6,9 +6,10 @@ Dependencies:
 
 Bugs fixed:
 â Bug 673918 â Port to newer libgee
+â Bug 629537 â Support anti-linking
 
 API changes:
-
+â Add AntiLinkable interface and implement it on Kf.Persona and Edsf.Persona
 
 Overview of changes from libfolks 0.7.1 to libfolks 0.7.2
 =========================================================
diff --git a/folks/Makefile.am b/folks/Makefile.am
index d283eb6..534ad09 100644
--- a/folks/Makefile.am
+++ b/folks/Makefile.am
@@ -90,6 +90,7 @@ libfolks_la_SOURCES = \
 	potential-match.vala \
 	avatar-cache.vala \
 	object-cache.vala \
+	anti-linkable.vala \
 	$(NULL)
 
 if ENABLE_EDS
diff --git a/folks/anti-linkable.vala b/folks/anti-linkable.vala
new file mode 100644
index 0000000..e90851e
--- /dev/null
+++ b/folks/anti-linkable.vala
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2011, 2012 Philip Withnall
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip tecnocode co uk>
+ */
+
+using Gee;
+using GLib;
+
+/**
+ * Interface for { link Persona} subclasses from backends which support storage
+ * of, anti-linking data.
+ *
+ * Anti-links are stored as a set of { link Persona.uid}s with each
+ * { link Persona} (A), specifying that A must not be linked into an
+ * { link Individual} with any of the personas in its anti-links set.
+ *
+ * @since UNRELEASED
+ */
+public interface Folks.AntiLinkable : Folks.Persona
+{
+  /**
+   * UIDs of anti-linked { link Persona}s.
+   *
+   * The { link Persona}s identified by their UIDs in this set are guaranteed to
+   * not be linked to this { link Persona}, even if their linkable properties
+   * match.
+   *
+   * No UIDs may be `null`. Well-formed but non-existent UIDs (i.e. UIDs which
+   * can be successfully parsed, but which don't currently correspond to a
+   * { link Persona} instance) are permitted, as personas may appear and
+   * disappear over time.
+   *
+   * It is expected, but not guaranteed, that anti-links made between personas
+   * will be reciprocal. That is, if persona A lists persona B's UID in its
+   * { link AntiLinkable.anti_links} set, persona B will typically also list
+   * persona A in its anti-links set.
+   *
+   * @since UNRELEASED
+   */
+  public abstract Set<string> anti_links { get; set; }
+
+  /**
+   * Change the { link Persona}'s set of anti-links.
+   *
+   * It's preferred to call this rather than setting
+   * { link AntiLinkable.anti_links} directly, as this method gives error
+   * notification and will only return once the anti-links have been written
+   * to the relevant backing store (or the operation's failed).
+   *
+   * It should be noted that { link IndividualAggregator.link_personas} and
+   * { link IndividualAggregator.unlink_individual} will modify the anti-links
+   * sets of the personas they touch, in order to remove and add anti-links,
+   * respectively. It is expected that these { link IndividualAggregator}
+   * methods will be used to modify anti-links indirectly, rather than calling
+   * { link AntiLinkable.change_anti_links} directly.
+   *
+   * @param anti_links the new set of anti-links from this persona
+   * @throws PropertyError if setting the anti-links failed
+   * @since UNRELEASED
+   */
+  public virtual async void change_anti_links (Set<string> anti_links)
+      throws PropertyError
+    {
+      /* Default implementation. */
+      throw new PropertyError.NOT_WRITEABLE (
+          _("Anti-links are not writeable on this contact."));
+    }
+
+  /**
+   * Check for an anti-link with another persona.
+   *
+   * This will return `true` if `other_persona`'s UID is listed in this
+   * persona's anti-links set. Note that this check is not symmetric.
+   *
+   * @param other_persona the persona to check is anti-linked
+   * @return `true` if an anti-link exists, `false` otherwise
+   * @since UNRELEASED
+   */
+  public bool has_anti_link_with_persona (Persona other_persona)
+    {
+      return (other_persona.uid in this.anti_links);
+    }
+
+  /**
+   * Add anti-links to other personas.
+   *
+   * The UIDs of all personas in `other_personas` will be added to this
+   * persona's anti-links set and the changes propagated to backends.
+   *
+   * Any attempt to anti-link a persona with itself is not an error, but is
+   * ignored.
+   *
+   * @param other_personas the personas to anti-link to this one
+   * @throws PropertyError if setting the anti-links failed
+   * @since UNRELEASED
+   */
+  public async void add_anti_links (Set<Persona> other_personas)
+      throws PropertyError
+    {
+      var new_anti_links = new HashSet<string> ();
+      new_anti_links.add_all (this.anti_links);
+
+      foreach (var p in other_personas)
+        {
+          /* Don't anti-link ourselves. */
+          if (p == this)
+            {
+              continue;
+            }
+
+          new_anti_links.add (p.uid);
+        }
+
+      yield this.change_anti_links (new_anti_links);
+    }
+
+  /**
+   * Remove anti-links to other personas.
+   *
+   * The UIDs of all personas in `other_personas` will be removed from this
+   * persona's anti-links set and the changes propagated to backends.
+   *
+   * @param other_personas the personas to remove anti-links from this one
+   * @throws PropertyError if setting the anti-links failed
+   * @since UNRELEASED
+   */
+  public async void remove_anti_links (Set<Persona> other_personas)
+      throws PropertyError
+    {
+      var new_anti_links = new HashSet<string> ();
+      new_anti_links.add_all (this.anti_links);
+
+      foreach (var p in other_personas)
+        {
+          new_anti_links.remove (p.uid);
+        }
+
+      yield this.change_anti_links (new_anti_links);
+    }
+}
+
+/* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */
diff --git a/folks/individual-aggregator.vala b/folks/individual-aggregator.vala
index 4bc1554..7774928 100644
--- a/folks/individual-aggregator.vala
+++ b/folks/individual-aggregator.vala
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2010 Collabora Ltd.
+ * Copyright (C) 2012 Philip Withnall
  *
  * 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
@@ -16,6 +17,7 @@
  *
  * Authors:
  *       Travis Reitter <travis reitter collabora co uk>
+ *       Philip Withnall <philip tecnocode co uk>
  */
 
 using Gee;
@@ -976,7 +978,8 @@ public class Folks.IndividualAggregator : Object
 
           /* If the Persona is the user, we *always* want to link it to the
            * existing this.user. */
-          if (persona.is_user == true && user != null)
+          if (persona.is_user == true && user != null &&
+              ((!) user).has_anti_link_with_persona (persona) == false)
             {
               debug ("    Found candidate individual '%s' as user.",
                   ((!) user).id);
@@ -994,6 +997,8 @@ public class Folks.IndividualAggregator : Object
                     {
                       if (candidate_ind != null &&
                           ((!) candidate_ind).trust_level != TrustLevel.NONE &&
+                          ((!) candidate_ind).has_anti_link_with_persona (
+                              persona) == false &&
                           candidate_inds.add ((!) candidate_ind))
                         {
                           debug ("    Found candidate individual '%s' by " +
@@ -1038,6 +1043,9 @@ public class Folks.IndividualAggregator : Object
                               if (candidate_ind != null &&
                                   ((!) candidate_ind).trust_level !=
                                       TrustLevel.NONE &&
+                                  ((!) candidate_ind).
+                                      has_anti_link_with_persona (
+                                          persona) == false &&
                                   candidate_inds.add ((!) candidate_ind))
                                 {
                                   debug ("    Found candidate individual '%s'" +
@@ -1165,6 +1173,24 @@ public class Folks.IndividualAggregator : Object
           null, null, GroupDetails.ChangeReason.NONE);
     }
 
+  private void _persona_anti_links_changed_cb (Object obj, ParamSpec pspec)
+    {
+      var persona = obj as Persona;
+
+      /* The anti-links associated with the persona 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. */
+      debug ("Anti-links changed for persona '%s' (is user: %s, IID: %s).",
+          persona.uid, persona.is_user ? "yes" : "no", persona.iid);
+
+      var persona_set = new HashSet<Persona> ();
+      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)
@@ -1172,10 +1198,23 @@ public class Folks.IndividualAggregator : Object
           persona.notify[prop_name].connect (
               this._persona_linkable_property_changed_cb);
         }
+
+      var al = persona as AntiLinkable;
+      if (al != null)
+        {
+          al.notify["anti-links"].connect (this._persona_anti_links_changed_cb);
+        }
     }
 
   private void _disconnect_from_persona (Persona persona)
     {
+      var al = persona as AntiLinkable;
+      if (al != null)
+        {
+          al.notify["anti-links"].disconnect (
+              this._persona_anti_links_changed_cb);
+        }
+
       foreach (var prop_name in persona.linkable_properties)
         {
           persona.notify[prop_name].disconnect (
@@ -1787,6 +1826,25 @@ public class Folks.IndividualAggregator : Object
           return;
         }
 
+      /* Remove all edges in the connected graph between the personas from the
+       * anti-link map to ensure that linking the personas actually succeeds. */
+      foreach (var p in personas)
+        {
+          var al = p as AntiLinkable;
+          if (al != null)
+            {
+              try
+                {
+                  yield ((!) al).remove_anti_links (personas);
+                }
+              catch (PropertyError e)
+                {
+                  throw new IndividualAggregatorError.PROPERTY_NOT_WRITEABLE (
+                      _("Anti-links can't be removed between personas being linked."));
+                }
+            }
+        }
+
       /* Create a new persona in the primary store which links together the
        * given personas */
       assert (((!) this._primary_store).type_id ==
@@ -1921,29 +1979,50 @@ public class Folks.IndividualAggregator : Object
           return;
         }
 
-      debug ("Unlinking Individual '%s', deleting Personas:", individual.id);
+      debug ("Unlinking Individual '%s':", individual.id);
 
-      /* Remove all the Personas from writeable PersonaStores.
+      /* Add all edges in the connected graph between the personas to the
+       * anti-link map to ensure that unlinking the personas actually succeeds,
+       * and that they aren't immediately re-linked.
        *
-       * 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<Persona> ();
-      foreach (var p in individual.personas)
-        {
-          personas.add (p);
-        }
+       * Perversely, this requires that we ensure the anti-links property is
+       * writeable on all personas before continuing. Ignore errors from it in
+       * the hope that everything works anyway.
+       *
+       * In the worst case, this will double the number of personas, since if
+       * none of the personas have anti-links writeable, each will have to be
+       * linked with a new writeable persona. */
+      var individual_personas = new HashSet<Persona> (); /* as we modify it */
+      individual_personas.add_all (individual.personas);
 
-      foreach (var persona in personas)
+      debug ("    Inserting anti-links:");
+      foreach (var pers in individual_personas)
         {
-          /* Since persona.store != null, we know that
-           * this._primary_store != null. */
-          if (persona.store == this._primary_store)
+          try
+            {
+              var personas = new HashSet<Persona> ();
+              personas.add (pers);
+              message ("Anti-linking persona '%s' (%p)", pers.uid, pers);
+
+              var writeable_persona =
+                  yield this._ensure_personas_property_writeable (personas,
+                      "anti-links");
+              message ("Writeable persona '%s' (%p)", writeable_persona.uid, writeable_persona);
+
+              /* Make sure not to anti-link the new persona to pers. */
+              var anti_link_personas = new HashSet<Persona> ();
+              anti_link_personas.add_all (individual_personas);
+              anti_link_personas.remove (pers);
+
+              var al = writeable_persona as AntiLinkable;
+              assert (al != null);
+              yield ((!) al).add_anti_links (anti_link_personas);
+              message ("");
+            }
+          catch (IndividualAggregatorError e1)
             {
-              debug ("    %s (is user: %s, IID: %s)", persona.uid,
-                  persona.is_user ? "yes" : "no", persona.iid);
-              yield ((!) this._primary_store).remove_persona (persona);
+              debug ("    Failed to ensure anti-links property is writeable " +
+                  "(continuing anyway): %s", e1.message);
             }
         }
     }
diff --git a/folks/individual.vala b/folks/individual.vala
index 8a612a7..06bcc32 100644
--- a/folks/individual.vala
+++ b/folks/individual.vala
@@ -2213,4 +2213,64 @@ public class Folks.Individual : Object,
     {
       this._set_personas (null, replacement_individual);
     }
+
+  /**
+   * Anti-linked with a persona?
+   *
+   * Check whether this individual is anti-linked to { link Persona} `p` at all.
+   * If so, `true` will be returned â `false` will be returned otherwise.
+   *
+   * Note that this will check for anti-links in either direction, since
+   * anti-links are not necessarily symmetric.
+   *
+   * @param p persona to check for anti-links with
+   * @return `true` if this individual is anti-linked with persona `p`; `false`
+   * otherwise
+   * @since UNRELEASED
+   */
+  public bool has_anti_link_with_persona (Persona p)
+    {
+      var al = p as AntiLinkable;
+
+      foreach (var persona in this._persona_set)
+        {
+          var pl = persona as AntiLinkable;
+
+          if ((al != null && ((!) al).has_anti_link_with_persona (persona)) ||
+              (pl != null && ((!) pl).has_anti_link_with_persona (p)))
+            {
+              return true;
+            }
+        }
+
+      return false;
+    }
+
+  /**
+   * Anti-linked with an individual?
+   *
+   * Check whether this individual is anti-linked to any of the { link Persona}s
+   * in { link Individual} `i`. If so, `true` will be returned â `false` will be
+   * returned otherwise.
+   *
+   * Note that this will check for anti-links in either direction, since
+   * anti-links are not necessarily symmetric.
+   *
+   * @param i individual to check for anti-links with
+   * @return `true` if this individual is anti-linked with individual `i`;
+   * `false` otherwise
+   * @since UNRELEASED
+   */
+  public bool has_anti_link_with_individual (Individual i)
+    {
+      foreach (var p in i.personas)
+        {
+          if (this.has_anti_link_with_persona (p) == true)
+            {
+              return true;
+            }
+        }
+
+      return false;
+    }
 }
diff --git a/folks/persona-store.vala b/folks/persona-store.vala
index 7019cde..6cea21e 100644
--- a/folks/persona-store.vala
+++ b/folks/persona-store.vala
@@ -294,7 +294,14 @@ public enum Folks.PersonaDetail
    *
    * @since 0.7.1
    */
-  LAST_CALL_INTERACTION_DATETIME
+  LAST_CALL_INTERACTION_DATETIME,
+
+  /**
+   * Field for { link AntiLinkable.anti_links}.
+   *
+   * @since UNRELEASED
+   */
+  ANTI_LINKS,
 }
 
 /**
@@ -353,7 +360,8 @@ public abstract class Folks.PersonaStore : Object
     "im-interaction-count",
     "last-im-interaction-datetime",
     "call-interaction-count",
-    "last-call-interaction-datetime"
+    "last-call-interaction-datetime",
+    "anti-links"
   };
 
   /**
diff --git a/folks/potential-match.vala b/folks/potential-match.vala
index 07a2504..ca998cd 100644
--- a/folks/potential-match.vala
+++ b/folks/potential-match.vala
@@ -34,7 +34,8 @@ public enum Folks.MatchResult
    *
    * This is used in situations where two individuals should never be linked,
    * such as when one of them has a { link Individual.trust_level} of
-   * { link TrustLevel.NONE}.
+   * { link TrustLevel.NONE}, or when the individuals are explicitly
+   * anti-linked.
    *
    * @since 0.6.8
    */
@@ -128,6 +129,13 @@ public class Folks.PotentialMatch : Object
           return result;
         }
 
+      /* Similarly, immediately discount a match if the individuals have been
+       * anti-linked by the user. */
+      if (a.has_anti_link_with_individual (b))
+        {
+          return result;
+        }
+
       result = MatchResult.VERY_LOW;
 
       /* If individuals share gender. */
diff --git a/tools/inspect/utils.vala b/tools/inspect/utils.vala
index cf5f98c..3774e63 100644
--- a/tools/inspect/utils.vala
+++ b/tools/inspect/utils.vala
@@ -252,7 +252,8 @@ private class Folks.Inspect.Utils
         }
       else if (prop_name == "groups" ||
                prop_name == "local-ids" ||
-               prop_name == "supported-fields")
+               prop_name == "supported-fields" ||
+               prop_name == "anti-links")
         {
           Set<string> groups = (Set<string>) prop_value.get_object ();
           output_string = "{ ";



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