'other messages by author' working
authorAndrew Sutherland <asutherland@asutherland.org>
Fri, 04 Jul 2008 21:45:49 -0700
changeset 827 97c8f4d91eb80dbdbd007bc471442f74e2bf60de
parent 826 59834a5d11d3aa7bc075158d7f8f8114cd43541b
child 828 7aba8e6fdd6615678878ca452a854e5b74be9847
push idunknown
push userunknown
push dateunknown
'other messages by author' working
modules/datamodel.js
modules/datastore.js
modules/explattr.js
modules/fundattr.js
modules/gloda.js
--- a/modules/datamodel.js
+++ b/modules/datamodel.js
@@ -71,23 +71,28 @@ GlodaAttributeDef.prototype = {
 
   /**
    * Bind a parameter value to the attribute definition, allowing use of the
    *  attribute-parameter as an attribute.
    *
    * @return 
    */
   bindParameter: function gloda_attr_bindParameter(aValue) {
+    // people probably shouldn't call us with null, but handle it
+    if (aValue == null) {
+      return this._id;
+    }
     if (aValue in this._parameterBindings) {
       return this._parameterBindings[aValue];
     }
     // no database entry exists if we are here, so we must create it...
-    let id = this._datastore.createAttributeDef(this._attrType,
+    let id = this._datastore._createAttributeDef(this._attrType,
                  this._pluginName, this._attrName, aValue);
     this._parameterBindings[aValue] = id;
+    this._datastore.reportBinding(id, this, aValue);
     return id;
   },  
 };
 
 function GlodaConversation(aDatastore, aID, aSubject, aOldestMessageDate,
                            aNewestMessageDate) {
   this._datastore = aDatastore;
   this._id = aID;
@@ -126,16 +131,17 @@ function GlodaMessage(aDatastore, aID, a
   this._conversation = aConversation;
   this._parentID = aParentID;
   this._headerMessageID = aHeaderMessageID;
   this._bodySnippet = aBodySnippet;
 
   // for now, let's always cache this; they should really be forgetting about us
   //  if they want to forget about the underlying storage anyways...
   this._folderMessage = null;
+  this._attributes = null;
 }
 
 GlodaMessage.prototype = {
   get id() { return this._id; },
   get folderID() { return this._folderID; },
   get messageKey() { return this._messageKey; },
   get conversationID() { return this._conversationID; },
   // conversation is special
@@ -173,19 +179,40 @@ GlodaMessage.prototype = {
       this._folderMessage = folder.GetMessageHeader(this._messageKey);
       return this._folderMessage;
     }
 
     throw "Unable to locate folder message for: " + this._folderURI + ":" +
           this._messageKey;
   },
   
-  clearAttributes: function gloda_attr_clearAttributes() {
+  get attributes() {
+    if (this._attributes == null) {
+      this._attributes = this._datastore.getMessageAttributes(this); 
+    }
+    
+    return this._attributes;
+  },
+  
+  clearAttributes: function gloda_message_clearAttributes() {
     this._datastore.clearMessageAttributes(this);
   },
+  
+  getAttributeInstances: function gloda_message_getAttributeInstances(aAttr) {
+    return [attrParamVal for each (attrParamVal in this.attributes) if
+            (attrParamVal[0] == aAttr)];
+  },
+  
+  getSingleAttribute: function gloda_message_getSingleAttribute(aAttr) {
+    let instances = this.getAttributeInstances(aAttr);
+    if (instances.length > 0)
+      return instances[0];
+    else
+      return null;
+  },
 };
 
 function GlodaContact(aDatastore, aID, aDirectoryUUID, aContactUUID, aName) {
   this._datastore = aDatastore;
   this._id = aID;
   this._directoryUUID = aDirectoryUUID;
   this._contactUUID = aContactUUID;
   this._name = aName;
--- a/modules/datastore.js
+++ b/modules/datastore.js
@@ -278,16 +278,21 @@ let GlodaDatastore = {
     
     let wrappedStatement = Cc["@mozilla.org/storage/statement-wrapper;1"].
                            createInstance(Ci.mozIStorageStatementWrapper);
     wrappedStatement.initialize(statement);
     return wrappedStatement;
   },
   
   /* ********** Attribute Definitions ********** */
+  /** Maps (attribute def) compound names to the GlodaAttributeDef objects. */
+  _attributes: {},
+  /** Map attribute ID to the definition and parameter value that produce it. */
+  _attributeIDToDef: {},
+  
   get _insertAttributeDefStatement() {
     let statement = this._createStatement(
       "INSERT INTO attributeDefinitions (attributeType, extensionName, name, \
                                   parameter) \
               VALUES (:attributeType, :extensionName, :name, :parameter)");
     this.__defineGetter__("_insertAttributeDefStatement", function() statement);
     return this._insertAttributeDefStatement; 
   },
@@ -318,17 +323,21 @@ let GlodaDatastore = {
       function() statement);
     return this._selectAttributeDefinitionsStatement;
   },
   
   /**
    * Look-up all the attribute definitions 
    */
   getAllAttributes: function gloda_ds_getAllAttributes() {
+    // map compound name to the attribute
     let attribs = {};
+    // map the attribute id to [attribute, parameter] where parameter is null
+    //  in cases where parameter is unused.
+    let idToAttribAndParam = {}
 
     this._log.info("loading all attribute defs");
     
     while (this._selectAttributeDefinitionsStatement.step()) {
       let row = this._selectAttributeDefinitionsStatement.row;
       
       let compoundName = row["extensionName"] + ":" + row["name"];
       
@@ -340,26 +349,33 @@ let GlodaDatastore = {
                                        compoundName, null, row["attributeType"],
                                        row["extensionName"], row["name"],
                                        null, null, null, null);
         attribs[compoundName] = attrib;
       }
       // if the parameter is null, the id goes on the attribute def, otherwise
       //  it is a parameter binding and goes in the binding map.
       if (row["parameter"] == null) {
-        attrib._id = row["id"]; 
+        attrib._id = row["id"];
+        idToAttribAndParam[row["id"]] = [attrib, null];
       } else {
         attrib._parameterBindings[row["parameter"]] = row["id"];
+        idToAttribAndParam[row["id"]] = [attrib, row["parameter"]];
       }
     }
     this._selectAttributeDefinitionsStatement.reset();
 
     this._log.info("done loading all attribute defs");
     
-    return attribs;
+    this._attributes = attribs;
+    this._attributeIDToDef = idToAttribAndParam;
+  },
+  
+  reportBinding: function gloda_ds_reportBinding(aID, aAttrDef, aParamValue) {
+    this._attributeIDToDef[aID] = [aAttrDef, aParamValue];
   },
   
   /* ********** Folders ********** */
   
   get _insertFolderLocationStatement() {
     let statement = this._createStatement(
       "INSERT INTO folderLocations (folderURI) VALUES (:folderURI)");
     this.__defineGetter__("_insertFolderLocationStatement",
@@ -714,16 +730,78 @@ let GlodaDatastore = {
   clearMessageAttributes: function gloda_ds_clearMessageAttributes(aMessage) {
     if (aMessage.id != null) {
       this._deleteMessageAttributesByMessageIDStatement.params.messageID =
         aMessage.id;
       this._deleteMessageAttributesByMessageIDStatement.execute();
     }
   },
   
+  get _selectMessageAttributesByMessageIDStatement() {
+    let statement = this._createStatement(
+      "SELECT * FROM messageAttributes WHERE messageID = :messageID");
+    this.__defineGetter__("_selectMessageAttributesByMessageIDStatement",
+      function() statement);
+    return this._selectMessageAttributesByMessageIDStatement;
+  },
+  
+  getMessageAttributes: function gloda_ds_getMessageAttributes(aMessage) {
+    // A list of [attribute def object, (attr) parameter value, attribute value]
+    let attribParamVals = []
+    
+    let smas = this._selectMessageAttributesByMessageIDStatement;
+    
+    smas.params.messageID = aMessage.id;
+    while (smas.step()) {
+      let attributeID = smas.row["attributeID"];
+      if (!(attributeID in this._attributeIDToDef)) {
+        this._log.error("Attribute ID " + attributeID + " not in our map!");
+      } 
+      let attribAndParam = this._attributeIDToDef[attributeID];
+      let val = smas.row["value"];
+      this._log.debug("Loading attribute: " + attribAndParam[0].id + " param: "+
+                      attribAndParam[1] + " val: " + val);
+      attribParamVals.push([attribAndParam[0], attribAndParam[1], val]);
+    }
+    smas.reset();
+    
+    return attribParamVals;
+  },
+  
+  queryMessagesAPV: function gloda_ds_queryMessagesAPV(aAPVs) {
+    let selects = [];
+    
+    for (let iAPV=0; iAPV < aAPVs.length; iAPV++) {
+      let APV = aAPVs[iAPV];
+      
+      let attributeID;
+      if (APV[1] != null)
+        attributeID = APV[0].bindParameter(APV[1]);
+      else
+        attributeID = APV[0].id;
+      let select = "SELECT messageID FROM messageAttributes WHERE attributeID" +
+                   " = " + attributeID;
+      if (APV[2] != null)
+        select += " AND value = " + APV[2];
+      selects.push(select);
+    }
+    
+    let sqlString = "SELECT * FROM messages WHERE id IN (" +
+                    selects.join(" INTERSECT ") + " )";
+    let statement = this._createStatement(sqlString);
+    
+    let messages = [];
+    while (statement.step()) {
+      messages.push(this._messageFromRow(statement.row));
+    }
+    statement.reset();
+     
+    return messages;
+  },
+  
   /* ********** Contact ********** */
   get _insertContactStatement() {
     let statement = this._createStatement(
       "INSERT INTO contacts (directoryUUID, contactUUID, name) \
               VALUES (:directoryUUID, :contactUUID, :name)");
     this.__defineGetter__("_insertContactStatement", function() statement);
     return this._insertContactStatement; 
   },
--- a/modules/explattr.js
+++ b/modules/explattr.js
@@ -75,28 +75,28 @@ let GlodaExplicitAttr = {
   },
 
   _attrTag: null,
   _attrStar: null,
   _attrRead: null,
   
   defineAttributes: function() {
     // Tag
-    this._attrTag = Gloda.defineAttr(this, Gloda.kAttrExplicit, EXT_BUILTIN,
-                        FA_TAG,
+    this._attrTag = Gloda.defineAttr(this, Gloda.kAttrExplicit,
+                        Gloda.BUILT_IN, FA_TAG,
                         Gloda.NOUN_MESSAGE, Gloda.NOUN_DATE, Gloda.NOUN_TAG,
                         "%{subject} was tagged %{parameter} on %{object}");
     // Star
-    this._attrStar = Gloda.defineAttr(this, Gloda.kAttrExplicit, EXT_BUILTIN,
-                        FA_STAR,
+    this._attrStar = Gloda.defineAttr(this, Gloda.kAttrExplicit,
+                        Gloda.BUILT_IN, FA_STAR,
                         Gloda.NOUN_MESSAGE, Gloda.NOUN_BOOLEAN, null,
                         "%{subject} has a star state of %{object}");
     // Read/Unread
-    this._attrRead = Gloda.defineAttr(this, Gloda.kAttrExplicit, EXT_BUILTIN,
-                        FA_READ,
+    this._attrRead = Gloda.defineAttr(this, Gloda.kAttrExplicit,
+                        Gloda.BUILT_IN, FA_READ,
                         Gloda.NOUN_MESSAGE, Gloda.NOUN_BOOLEAN, null,
                         "%{subject} has a read state of %{object}");
     
   },
   
   process: function Gloda_explattr_process(aGlodaMessage, aMsgHdr) {
     let attribs = [];
     
--- a/modules/fundattr.js
+++ b/modules/fundattr.js
@@ -43,17 +43,16 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://gloda/modules/log4moz.js");
 
 Cu.import("resource://gloda/modules/utils.js");
 Cu.import("resource://gloda/modules/gloda.js");
 Cu.import("resource://gloda/modules/datastore.js");
 
-const EXT_BUILTIN = "built-in";
 const FA_FROM = "FROM";
 const FA_TO = "TO";
 const FA_CC = "CC";
 const FA_DATE = "DATE";
 
 /**
  * The Gloda Fundamental Attribute provider is a special-case attribute
  *  provider; it provides attributes that the rest of the providers should be
@@ -78,50 +77,54 @@ let GlodaFundAttr = {
 
   _attrFrom: null,
   _attrTo: null,
   _attrCc: null,
   _attrDate: null,
   
   defineAttributes: function() {
     // From
-    this._attrFrom = Gloda.defineAttr(this, Gloda.kAttrFundamental, EXT_BUILTIN,
-                        FA_FROM,
+    this._attrFrom = Gloda.defineAttr(this, Gloda.kAttrFundamental,
+                        Gloda.BUILT_IN, FA_FROM,
                         Gloda.NOUN_MESSAGE, Gloda.NOUN_IDENTITY, null,
                         "%{subject} was sent by %{object}");
     // To
-    this._attrTo = Gloda.defineAttr(this, Gloda.kAttrFundamental, EXT_BUILTIN,
-                        FA_TO,
+    this._attrTo = Gloda.defineAttr(this, Gloda.kAttrFundamental,
+                        Gloda.BUILT_IN, FA_TO,
                         Gloda.NOUN_MESSAGE, Gloda.NOUN_IDENTITY, null,
                         "%{subject} was sent to %{object}");
     // Cc
-    this._attrCc = Gloda.defineAttr(this, Gloda.kAttrFundamental, EXT_BUILTIN,
-                        FA_CC,
+    this._attrCc = Gloda.defineAttr(this, Gloda.kAttrFundamental,
+                        Gloda.BUILT_IN, FA_CC,
                         Gloda.NOUN_MESSAGE, Gloda.NOUN_IDENTITY, null,
                         "%{subject} was carbon-copied to %{object}");
     // Date
-    this._attrDate = Gloda.defineAttr(this, Gloda.kAttrFundamental, EXT_BUILTIN,
-                        FA_DATE,
+    this._attrDate = Gloda.defineAttr(this, Gloda.kAttrFundamental,
+                        Gloda.BUILT_IN, FA_DATE,
                         Gloda.NOUN_MESSAGE, Gloda.NOUN_DATE, null,
                         "%{subject} was sent on %{object}");
     
   },
   
   process: function gloda_fundattr_process(aGlodaMessage, aMsgHdr) {
     let attribs = [];
     
     // -- From
     // Let's use replyTo if available.
+    // er, since we are just dealing with mailing lists for now, forget the
+    //  reply-to...
     // TODO: deal with default charset issues
     let author = null;
+    /*
     try {
       author = aMsgHdr.getStringProperty("replyTo");
     }
     catch (ex) {
     }
+    */
     if (author == null || author == "")
       author = aMsgHdr.author;
 
     let authorIdentity = Gloda.getIdentityForFullMailAddress(author);
     if (authorIdentity == null) {
       this._log.error("Message with subject '" + aMsgHdr.mime2DecodedSubject +
                       "' somehow lacks a valid author.  Bailing.");
       return attribs;
--- a/modules/gloda.js
+++ b/modules/gloda.js
@@ -131,33 +131,33 @@ let Gloda = {
   },
   
   kAttrFundamental: 0,
   kAttrOptimization: 1,
   kAttrDerived: 2,
   kAttrExplicit: 3,
   kAttrImplicit: 4,
   
+  BUILT_IN: "built-in",
+  
   /** A date, encoded as a PRTime */
   NOUN_DATE: 10,
   NOUN_TAG: 50,
   NOUN_CONVERSATION: 101,
   NOUN_MESSAGE: 102,
   NOUN_CONTACT: 103,
   NOUN_IDENTITY: 104,
   
   /** Attribute providers in the sequence to process them. */
   _attrProviderOrder: [],
   /** Maps attribute providers to the list of attributes they provide */
   _attrProviders: {},
-  /** Maps (attribute def) compound names to the GlodaAttributeDef objects. */
-  _attributes: {},
   
   _initAttributes: function gloda_ns_initAttributes() {
-    this._attributes = GlodaDatastore.getAllAttributes();
+    GlodaDatastore.getAllAttributes();
   },
   
   /**
    * @param aProvider
    * @param aAttrType
    * @param aPluginName
    * @param aAttrName
    * @param aSubjectType
@@ -172,22 +172,22 @@ let Gloda = {
     // provider tracking
     if (!(aProvider in this._attrProviders)) {
       this._attrProviderOrder.push(aProvider);
       this._attrProviders[aProvider] = [];
     } 
     
     let compoundName = aPluginName + ":" + aAttrName;
     let attr = null;
-    if (compoundName in this._attributes) {
+    if (compoundName in GlodaDatastore._attributes) {
       // the existence of the GlodaAttributeDef means that either it has
       //  already been fully defined, or has been loaded from the database but
       //  not yet 'bound' to a provider (and had important meta-info that
       //  doesn't go in the db copied over)
-      attr = this._attributes[compoundName];
+      attr = GlodaDatastore._attributes[compoundName];
       if (attr.provider != null) {
         return attr;
       }
       
       // we are behind the abstraction veil and can set these things
       attr._provider = aProvider;
       attr._subjectType = aSubjectType;
       attr._objectType = aObjectType;
@@ -200,27 +200,33 @@ let Gloda = {
     
     // Being here means the attribute def does not exist in the database.
     // Of course, we only want to create something in the database if the
     //  parameter is forever un-bound (type is null).
     let attrID = null;
     if (aParameterType == null) {
       attrID = GlodaDatastore._createAttributeDef(aAttrType, aPluginName,
                                                   aAttrName, null);
+      GlodaDatastore._attributeIDToDef[attrID] = [attrID, null];
     }
     
     attr = new GlodaAttributeDef(GlodaDatastore, attrID, compoundName,
                                  aProvider, aAttrType, aPluginName, aAttrName,
                                  aSubjectType, aObjectType, aParameterType,
                                  aExplanationFormat);
-    this._attributes[compoundName] = attr;
+    GlodaDatastore._attributes[compoundName] = attr;
     this._attrProviders[aProvider].push(attr);
     return attr;
   },
   
+  getAttrDef: function gloda_ns_getAttrDef(aPluginName, aAttrName) {
+    let compoundName = aPluginName + ":" + aAttrName;
+    return GlodaDatastore._attributes[compoundName];
+  },
+  
   processMessage: function gloda_ns_processMessage(aMessage, aMsgHdr) {
     // For now, we are ridiculously lazy and simply nuke all existing attributes
     //  before applying the new attributes.
     aMessage.clearAttributes();
     
     let allAttribs = [];
   
     for(let i = 0; i < this._attrProviderOrder.length; i++) {
@@ -253,11 +259,15 @@ let Gloda = {
         outAttribs.push([attribID, attribDesc[2]]);
       }
     }
     
     this._log.debug("Attributes: " + outAttribs);
     
     GlodaDatastore.insertMessageAttributes(aMessage, outAttribs);
   },
+  
+  queryMessagesAPV: function gloda_ns_queryMessagesAPV(aAPVs) {
+    return GlodaDatastore.queryMessagesAPV(aAPVs);
+  },
 };
 
 Gloda._init();