factor out feed subscription and refreshment into a single code path that handles both; separate retrieval from storage in the identity module (not complete; we still have to handle the case where an identity/person already exists)

--HG--
extra : rebase_source : c2fde49d0d6bb0da213e5722543e6e6631e677a9
This commit is contained in:
Myk Melez 2009-05-07 18:45:17 -07:00
Родитель e853161c96
Коммит 81e697455e
4 изменённых файлов: 212 добавлений и 182 удалений

Просмотреть файл

@ -181,13 +181,29 @@ SnowlFeed.prototype = {
throw Cr.NS_ERROR_NOT_IMPLEMENTED; throw Cr.NS_ERROR_NOT_IMPLEMENTED;
}, },
//**************************************************************************//
// Refreshment
_refreshTime: null, _refreshTime: null,
_refreshCallback: null,
refresh: function(refreshTime) { /**
// Cache the refresh time so we can use it as the received time when adding * Refresh the feed, retrieving the latest information in it.
// messages to the datastore. *
this._refreshTime = refreshTime; * @param time {Date}
* The time the refresh was initiated; determines new messages'
* received time. We let the caller specify this so a caller
* refreshing multiple feeds can give their messages the same
* received time.
* @param callback {Function}
*/
refresh: function(time, callback) {
this._refreshTime = time;
this._refreshCallback = callback;
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:connect:start", this);
this._log.info("refreshing " + this.machineURI.spec); this._log.info("refreshing " + this.machineURI.spec);
new Request({ new Request({
@ -210,24 +226,29 @@ SnowlFeed.prototype = {
// period of time. We should instead keep trying when a source fails, // period of time. We should instead keep trying when a source fails,
// but with a progressively longer interval (up to the standard one). // but with a progressively longer interval (up to the standard one).
// FIXME: implement the approach described above. // FIXME: implement the approach described above.
this.lastRefreshed = refreshTime; this.lastRefreshed = time;
}, },
onRefreshLoad: function(aEvent) { onRefreshLoad: function(event) {
let request = aEvent.target; let request = event.target;
// The load event can fire even with a non 2xx code, so handle as error // The load event can fire even with a non 2xx code, so handle as error
if (request.status < 200 || request.status > 299) { if (request.status < 200 || request.status > 299) {
this.onRefreshError(aEvent); this.onRefreshError(event);
return; return;
} }
// XXX What's the right way to handle this? // XXX What's the right way to handle this?
if (request.responseText.length == 0) { if (request.responseText.length == 0) {
this.onRefreshError(aEvent); this.onRefreshError(event);
return; return;
} }
// XXX Perhaps we should set this._lastStatus = request.status so we don't
// need to pass it in this notification and it's available at any time.
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:connect:end", this, request.status);
// _authInfo only gets set if we prompted the user to authenticate // _authInfo only gets set if we prompted the user to authenticate
// and the user checked the "remember password" box. Since we're here, // and the user checked the "remember password" box. Since we're here,
// it means the request succeeded, so we save the login. // it means the request succeeded, so we save the login.
@ -249,38 +270,77 @@ SnowlFeed.prototype = {
this._resetRefresh(); this._resetRefresh();
}, },
onRefreshError: function(aEvent) { onRefreshError: function(event) {
let request = aEvent.target; let request = event.target;
// Sometimes an attempt to retrieve status text throws NS_ERROR_NOT_AVAILABLE. // Sometimes an attempt to retrieve status text throws NS_ERROR_NOT_AVAILABLE.
let statusText; let statusText;
try {statusText = request.statusText;} catch(ex) {statusText = "[no status text]"} try { statusText = request.statusText } catch(ex) { statusText = "[no status text]" }
this._log.error("onRefreshError: " + request.status + " (" + statusText + ")"); this._log.error("onRefreshError: " + request.status + " (" + statusText + ")");
// XXX Perhaps we should set this._lastStatus = request.status so we don't
// need to pass it in this notification and it's available at any time.
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:connect:end", this, request.status);
this._resetRefresh(); this._resetRefresh();
if (this._subscribeCallback)
this._subscribeCallback();
}, },
onRefreshResult: strand(function(aResult) { onRefreshResult: strand(function(result) {
// FIXME: figure out why aResult.doc is sometimes null (its content isn't // FIXME: figure out why result.doc is sometimes null (perhaps its content
// a valid feed?) and report a more descriptive error message. // isn't a valid feed?) and report a more descriptive error message.
if (aResult.doc == null) { if (result.doc == null) {
this._log.error("onRefreshResult: result.doc is null"); this._log.error("onRefreshResult: result.doc is null");
// FIXME: factor this out with similar code in onSubscribeError and make
// the observers of snowl:subscribe:connect:end understand the status
// we return.
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:connect:end", this, "result.doc is null");
if (this._subscribeCallback)
this._subscribeCallback();
return; return;
} }
let feed = aResult.doc.QueryInterface(Components.interfaces.nsIFeed); try {
let feed = result.doc.QueryInterface(Ci.nsIFeed);
this.messages = this._processFeed(feed, this._refreshTime); // Extract the name and human URI (if we don't already have them)
if (this.id) // from the feed.
this.persistMessages(); // ??? Should we update these if they've changed?
Observers.notify("snowl:refresh:end", this); if (!this.name)
this.name = feed.title.plainText();
if (!this.humanURI)
this.humanURI = feed.link;
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:get:start", this);
this.messages = this._processFeed(feed, this._refreshTime);
// FIXME: remove subscribe from this notification's name.
Observers.notify("snowl:subscribe:get:end", this);
}
catch(ex) {
this._log.error("error on subscribe result: " + ex);
// FIXME: remove subscribe from this notification's name.
// FIXME: make this something besides "connect:end" since we've already
// issued one of those notifications by now.
Observers.notify("snowl:subscribe:connect:end", this, "error: " + ex);
}
finally {
if (this._subscribeCallback)
this._subscribeCallback();
}
}), }),
_resetRefresh: function() { _resetRefresh: function() {
this._refreshTime = null; this._refreshTime = null;
}, },
//**************************************************************************//
// Processing
/** /**
* Process a feed into an array of messages. * Process a feed into an array of messages.
* *
@ -298,7 +358,12 @@ SnowlFeed.prototype = {
// one for it based on its content. // one for it based on its content.
try { try {
let externalID = entry.id || this._generateID(entry); let externalID = entry.id || this._generateID(entry);
if (typeof externalID == "undefined")
dump("no external ID\n");
else
dump("external ID: " + externalID + "\n");
let message = this._processEntry(feed, entry, externalID, received); let message = this._processEntry(feed, entry, externalID, received);
dump("processed entry into message\n");
messages.push(message); messages.push(message);
} }
catch(ex) { catch(ex) {
@ -334,6 +399,7 @@ SnowlFeed.prototype = {
let authors = (aEntry.authors.length > 0) ? aEntry.authors let authors = (aEntry.authors.length > 0) ? aEntry.authors
: (aFeed.authors.length > 0) ? aFeed.authors : (aFeed.authors.length > 0) ? aFeed.authors
: null; : null;
// FIXME: process all authors, not just the first one.
if (authors && authors.length > 0) { if (authors && authors.length > 0) {
let author = authors.queryElementAt(0, Ci.nsIFeedPerson); let author = authors.queryElementAt(0, Ci.nsIFeedPerson);
// The external ID for an author is her email address, if provided // The external ID for an author is her email address, if provided
@ -342,14 +408,9 @@ SnowlFeed.prototype = {
// email address if a name is not provided (which it probably was). // email address if a name is not provided (which it probably was).
let externalID = author.email || author.name; let externalID = author.email || author.name;
let name = author.name || author.email; let name = author.name || author.email;
message.author = new SnowlIdentity(null, this, externalID, name);
// Get an existing identity or create a new one. Creating an identity //identity = SnowlIdentity.get(this.id, externalID) ||
// automatically creates a person record with the provided name. // SnowlIdentity.create(this.id, externalID, name);
identity = SnowlIdentity.get(this.id, externalID) ||
SnowlIdentity.create(this.id, externalID, name);
message.authorID = identity.personID;
// message.authorName
// message.authorIcon
} }
// Add parts // Add parts
@ -395,122 +456,6 @@ SnowlFeed.prototype = {
return "urn:" + hasher.finish(true); return "urn:" + hasher.finish(true);
}, },
//**************************************************************************//
// Subscription
_subscribeCallback: null,
subscribe: function(callback) {
Observers.notify("snowl:subscribe:connect:start", this);
this._subscribeCallback = callback;
this._log.info("subscribing to " + this.machineURI.spec);
let request = new Request({
loadCallback: new Callback(this.onSubscribeLoad, this),
errorCallback: new Callback(this.onSubscribeError, this),
// The feed processor is going to parse the XML, so override the MIME type
// in order to turn off parsing by XMLHttpRequest itself.
overrideMimeType: "text/plain",
url: this.machineURI,
// Register a listener for notification callbacks so we handle
// authentication.
notificationCallbacks: this
});
},
onSubscribeLoad: function(aEvent) {
let request = aEvent.target;
// The load event can fire even with a non 2xx code, so handle as error
if (request.status < 200 || request.status > 299) {
this.onSubscribeError(aEvent);
return;
}
// XXX What's the right way to handle this?
if (request.responseText.length == 0) {
this.onSubscribeError(aEvent);
return;
}
Observers.notify("snowl:subscribe:connect:end", this, request.status);
// _authInfo only gets set if we prompted the user to authenticate
// and the user checked the "remember password" box. Since we're here,
// it means the request succeeded, so we save the login.
if (this._authInfo)
this._saveLogin();
let parser = Cc["@mozilla.org/feed-processor;1"].
createInstance(Ci.nsIFeedProcessor);
parser.listener = {
self: this,
handleResult: function(result) {
this.self.onSubscribeResult(result);
}
};
parser.parseFromString(request.responseText, request.channel.URI);
},
onSubscribeError: function(aEvent) {
let request = aEvent.target;
// Sometimes an attempt to retrieve status text throws NS_ERROR_NOT_AVAILABLE.
let statusText;
try { statusText = request.statusText } catch(ex) { statusText = "[no status text]" }
this._log.error("onSubscribeError: " + request.status + " (" + statusText + ")");
Observers.notify("snowl:subscribe:connect:end", this, request.status);
if (this._subscribeCallback)
this._subscribeCallback();
},
onSubscribeResult: strand(function(aResult) {
// FIXME: figure out why aResult.doc is sometimes null (its content isn't
// a valid feed?) and report a more descriptive error message.
if (aResult.doc == null) {
this._log.error("result.doc is null");
// FIXME: factor this out with similar code in onSubscribeError and make
// the observers of snowl:subscribe:connect:end understand the status
// we return.
Observers.notify("snowl:subscribe:connect:end", this, "result.doc is null");
if (this._subscribeCallback)
this._subscribeCallback();
return;
}
try {
let feed = aResult.doc.QueryInterface(Ci.nsIFeed);
// Extract the name (if we don't already have one) and human URI from the feed.
if (!this.name)
this.name = feed.title.plainText();
this.humanURI = feed.link;
this.persist();
//Observers.notify("snowl:sources:changed");
// FIXME: use a date provided by the subscriber so refresh times are the same
// for all accounts subscribed at the same time (f.e. in an OPML import).
Observers.notify("snowl:subscribe:get:start", this);
this.messages = this._processFeed(feed, new Date());
this.persistMessages();
Observers.notify("snowl:subscribe:get:end", this);
}
catch(ex) {
this._log.error("error on subscribe result: " + feed.toSource());
this._log.error("error on subscribe result: " + ex);
Observers.notify("snowl:subscribe:connect:end", this, "error:" + ex);
}
finally {
if (this._subscribeCallback)
this._subscribeCallback();
}
}),
_saveLogin: function() { _saveLogin: function() {
let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); let lm = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);

Просмотреть файл

@ -46,39 +46,129 @@ Cu.import("resource://snowl/modules/datastore.js");
Cu.import("resource://snowl/modules/source.js"); Cu.import("resource://snowl/modules/source.js");
Cu.import("resource://snowl/modules/URI.js"); Cu.import("resource://snowl/modules/URI.js");
function SnowlIdentity(id, sourceID, externalID, personID) {
function SnowlIdentity(id, source, externalID, name) {
this.id = id; this.id = id;
this.sourceID = sourceID; this.source = source;
this.externalID = externalID; this.externalID = externalID;
this.personID = personID; this.person = new SnowlPerson(null, name);
} }
SnowlIdentity.get = function(sourceID, externalID) { SnowlIdentity.prototype = {
let identity; id: null,
source: null,
externalID: null,
person: null,
let statement = SnowlDatastore.createStatement( persist: function() {
"SELECT id, personID " + this.person.persist(this.source.id);
"FROM identities " +
"WHERE externalID = :externalID AND sourceID = :sourceID"
);
try { let statement = SnowlDatastore.createStatement(
statement.params.sourceID = sourceID; "INSERT INTO identities ( sourceID, externalID, personID) " +
statement.params.externalID = externalID; "VALUES (:sourceID, :externalID, :personID)"
if (statement.step()) { );
identity = new SnowlIdentity(statement.row.id,
sourceID, try {
externalID, statement.params.sourceID = this.source.id;
statement.row.personID); statement.params.externalID = this.externalID;
statement.params.personID = this.person.id;
statement.step();
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
}
finally {
statement.reset();
} }
} }
finally {
statement.reset(); };
function SnowlPerson(id, name, placeID, homeURL, iconURL) {
this.id = id;
this.name = name;
this.placeID = placeID;
}
SnowlPerson.prototype = {
name: null,
placeID: null,
homeURL: null,
iconURL: null,
persist: function(sourceID) {
let statement = SnowlDatastore.createStatement(
"INSERT INTO people ( name, homeURL, iconURL) " +
"VALUES (:name, :homeURL, :iconURL)"
);
try {
statement.params.name = this.name;
statement.params.homeURL = this.homeURL;
statement.params.iconURL = this.iconURL;
statement.step();
this.id = SnowlDatastore.dbConnection.lastInsertRowID;
// XXX lookup favicon in collections table rather than hardcoding
let iconURI =
this.iconURL ? URI.get(this.iconURL) :
this.homeURL ? SnowlSource.faviconSvc.getFaviconForPage(this.homeURL) :
URI.get("chrome://snowl/skin/person-16.png");
// Create places record, placeID stored into people table record.
//SnowlPlaces._log.info("Author name:iconURI.spec - " + name + " : " + iconURI.spec);
// FIXME: break the dependency on sourceID, since people should only be
// connected to sources through identities.
this.placeID = SnowlPlaces.persistPlace("people",
this.id,
name,
null, // homeURL,
externalID, // externalID,
iconURI,
sourceID);
// Store placeID back into messages for DB integrity.
SnowlDatastore.dbConnection.executeSimpleSQL(
"UPDATE people " +
"SET placeID = " + this.placeID +
" WHERE id = " + this.id);
}
finally {
statement.reset();
}
} }
return identity;
}; };
//SnowlIdentity.get = function(sourceID, externalID) {
// let identity;
//
// let statement = SnowlDatastore.createStatement(
// "SELECT id, personID " +
// "FROM identities " +
// "WHERE externalID = :externalID AND sourceID = :sourceID"
// );
//
// try {
// statement.params.sourceID = sourceID;
// statement.params.externalID = externalID;
// if (statement.step()) {
// identity = new SnowlIdentity(statement.row.id,
// sourceID,
// externalID,
// statement.row.personID);
// }
// }
// finally {
// statement.reset();
// }
//
// return identity;
//};
SnowlIdentity.create = function(sourceID, externalID, name, homeURL, iconURL) { SnowlIdentity.create = function(sourceID, externalID, name, homeURL, iconURL) {
let identity; let identity;
@ -136,14 +226,6 @@ SnowlIdentity.create = function(sourceID, externalID, name, homeURL, iconURL) {
return identity; return identity;
}; };
SnowlIdentity.prototype = {};
function SnowlPerson(id, name, placeID) {
this.id = id;
this.name = name;
this.placeID = placeID;
}
SnowlPerson.__defineGetter__("_getAllStatement", SnowlPerson.__defineGetter__("_getAllStatement",
function() { function() {
let statement = SnowlDatastore.createStatement( let statement = SnowlDatastore.createStatement(

Просмотреть файл

@ -219,10 +219,12 @@ SnowlMessage.prototype = {
SnowlDatastore.dbConnection.beginTransaction(); SnowlDatastore.dbConnection.beginTransaction();
try { try {
this.author.persist();
this._stmtInsertMessage.params.sourceID = this.sourceID; this._stmtInsertMessage.params.sourceID = this.sourceID;
this._stmtInsertMessage.params.externalID = this.externalID; this._stmtInsertMessage.params.externalID = this.externalID;
this._stmtInsertMessage.params.subject = this.subject; this._stmtInsertMessage.params.subject = this.subject;
this._stmtInsertMessage.params.authorID = this.authorID; this._stmtInsertMessage.params.authorID = this.author.id;
this._stmtInsertMessage.params.timestamp = SnowlDateUtils.jsToJulianDate(this.timestamp); this._stmtInsertMessage.params.timestamp = SnowlDateUtils.jsToJulianDate(this.timestamp);
this._stmtInsertMessage.params.received = SnowlDateUtils.jsToJulianDate(this.received); this._stmtInsertMessage.params.received = SnowlDateUtils.jsToJulianDate(this.received);
this._stmtInsertMessage.params.link = this.link ? this.link.spec : null; this._stmtInsertMessage.params.link = this.link ? this.link.spec : null;

Просмотреть файл

@ -22,17 +22,15 @@ function run_test() {
do_test_pending(); do_test_pending();
Observers.add("snowl:subscribe:get:end", continue_test); Observers.add("snowl:subscribe:get:end", finish_test);
feed = new SnowlFeed(null, null, new URI("http://localhost:8080/feed.xml"), undefined, null); feed = new SnowlFeed(null, null, new URI("http://localhost:8080/feed.xml"), undefined, null);
feed.subscribe();
}
function continue_test() {
Observers.add("snowl:refresh:end", finish_test);
feed.refresh(refreshTime); feed.refresh(refreshTime);
} }
function finish_test() { function finish_test() {
feed.persist();
feed.persistMessages();
try { try {
do_check_eq(SnowlService.accounts.length, 1); do_check_eq(SnowlService.accounts.length, 1);
let account = SnowlService.accounts[0]; let account = SnowlService.accounts[0];
@ -55,6 +53,9 @@ function finish_test() {
do_check_eq(message.subject, "Atom-Powered Robots Run Amok"); do_check_eq(message.subject, "Atom-Powered Robots Run Amok");
do_check_eq(message.authorName, "John Doe"); do_check_eq(message.authorName, "John Doe");
// TODO: do_check_eq(message.authorID, authorID); // TODO: do_check_eq(message.authorID, authorID);
// TODO: test that the message's author is a real identity record
// with a real person record behind it and the values of those records
// are all correct.
do_check_eq(message.link, "http://example.org/2003/12/13/atom03"); do_check_eq(message.link, "http://example.org/2003/12/13/atom03");
do_check_eq(message.timestamp.getTime(), 1071340202000); do_check_eq(message.timestamp.getTime(), 1071340202000);
do_check_eq(message._read, false); do_check_eq(message._read, false);