Fix bug 490309 - Implement asynchronous ical parsing. r=philipp

This commit is contained in:
Daniel Boelzle [:dbo] 2009-05-19 21:58:14 +02:00
Родитель bad689f77e
Коммит 003b13ff38
16 изменённых файлов: 283 добавлений и 113 удалений

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

@ -1,4 +1,3 @@
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
@ -21,6 +20,7 @@
*
* Contributor(s):
* Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
* Daniel Boelzle <mozilla@boelzle.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -37,9 +37,6 @@
* ***** END LICENSE BLOCK ***** */
#include "nsIGenericFactory.h"
#include "nsIClassInfoImpl.h"
#include "calDateTime.h"
#include "calDuration.h"
#include "calPeriod.h"
@ -69,7 +66,6 @@ NS_GENERIC_FACTORY_CONSTRUCTOR(calDateTime)
NS_DECL_CLASSINFO(calDateTime)
NS_GENERIC_FACTORY_CONSTRUCTOR(calICSService)
NS_DECL_CLASSINFO(calICSService)
static const nsModuleComponentInfo components[] =
{
@ -109,13 +105,7 @@ static const nsModuleComponentInfo components[] =
{ "ICS parser/serializer",
CAL_ICSSERVICE_CID,
CAL_ICSSERVICE_CONTRACTID,
calICSServiceConstructor,
NULL,
NULL,
NULL,
NS_CI_INTERFACE_GETTER_NAME(calICSService),
NULL,
&NS_CLASSINFO_NAME(calICSService)
calICSServiceConstructor
},
{ "Calendar Recurrence Rule",
CAL_RECURRENCERULE_CID,

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

@ -176,7 +176,7 @@ function pasteFromClipboard() {
let icsParser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Components.interfaces.calIIcsParser);
try {
icsParser.parseString(data, null);
icsParser.parseString(data);
} catch(e) {}
let items = icsParser.getItems({});

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

@ -292,7 +292,7 @@ calDNDBaseObserver.prototype = {
#endif
var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Components.interfaces.calIIcsParser);
parser.parseString(data, null);
parser.parseString(data);
this.onDropItems(parser.getItems({}).concat(parser.getParentlessItems({})));
break;
case "text/unicode":
@ -357,7 +357,7 @@ calDNDBaseObserver.prototype = {
}
var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Components.interfaces.calIIcsParser);
parser.parseString(str, null);
parser.parseString(str);
self.onDropItems(parser.getItems({}).concat(parser.getParentlessItems({})));
}
}

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

@ -99,6 +99,77 @@ let cal = {
thread.processNextEvent(false /* don't wait */);
},
get threadingEnabled cal_threadingEnabled() {
if (cal_threadingEnabled.val === undefined) {
cal_threadingEnabled.val = !cal.getPrefSafe("calendar.threading.disabled", false);
}
return cal_threadingEnabled.val;
},
getWorkerThread: function cal_getWorkerThread() {
let threads = cal_getWorkerThread.mThreads;
switch (threads) {
case null:
return null; // during shutdown
case undefined: {
cal_getWorkerThread.mThreads = threads = Components.classes["@mozilla.org/thread-pool;1"]
.createInstance(Components.interfaces.nsIThreadPool);
cal.addShutdownObserver(function() {
cal.LOG("cal.getWorkerThread() shutdown.");
let threads = cal_getWorkerThread.mThreads;
cal_getWorkerThread.mThreads = null;
threads.shutdown();
});
break;
}
}
return threads;
},
/**
* Executes the passed action function on a worker thread.
* Once the action function has been executed, respFunc is called
* on the same thread that execAsync() has been called on.
*
* xxx todo: think of leaks, ref cycles!
*/
execWorker: function cal_execWorker(actionFunc, respFunc) {
// response executed on the calling thread:
let callingThread = cal.getThreadManager().currentThread;
let worker = { // nsIRunnable:
run: function worker_run() {
let res = null;
try {
actionFunc(callingThread);
} catch (exc) {
res = exc;
}
if (!respFunc) {
return;
}
let response = { // nsIRunnable:
run: function response_run() {
respFunc(res);
}
};
callingThread.dispatch(response, Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
}
};
if (cal.threadingEnabled) {
let thread = cal.getWorkerThread();
if (thread) {
thread.dispatch(worker, Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
return;
} // else shutdown ongoing
}
runnable.run(); // exec on current thread
},
/**
* Checks whether a timezone lacks a definition.
*/
@ -473,6 +544,40 @@ let cal = {
wm.getMostRecentWindow("mail:3pane");
},
/**
* Adds an observer listening for the topic.
*
* @param func function to execute on topic
* @param topic topic to listen for
* @param oneTime whether to listen only once
*/
addObserver: function cal_addObserver(func, topic, oneTime) {
let observer = { // nsIObserver:
observe: function cal_addObserver_observe(subject, topic_, data) {
if (topic == topic_) {
if (oneTime) {
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService)
.removeObserver(this, topic);
}
func(subject, topic, data);
}
}
};
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService)
.addObserver(observer, topic, false /* don't hold weakly */);
},
/**
* Adds an xpcom shutdown observer.
*
* @param func function to execute
*/
addShutdownObserver: function cal_addShutdownObserver(func) {
cal.addObserver(func, "xpcom-shutdown", true /* one time */);
},
/**
* Due to wrapped js objects, some objects may have cyclic references.
* You can register properties of objects to be cleaned up on xpcom-shutdown.
@ -488,28 +593,18 @@ let cal = {
// will be used to clean up global objects on shutdown
// some objects have cyclic references due to wrappers
function shutdownCleanup(obj, prop) {
if (!shutdownCleanup.mObserver) {
if (!shutdownCleanup.mEntries) {
shutdownCleanup.mEntries = [];
shutdownCleanup.mObserver = { // nsIObserver:
observe: function shutdownCleanup_observe(subject, topic, data) {
if (topic == "xpcom-shutdown") {
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService)
.removeObserver(this, "xpcom-shutdown");
for each (let entry in shutdownCleanup.mEntries) {
if (entry.mProp) {
delete entry.mObj[entry.mProp];
} else {
delete entry.mObj;
}
cal.addShutdownObserver(function() {
for each (let entry in shutdownCleanup.mEntries) {
if (entry.mProp) {
delete entry.mObj[entry.mProp];
} else {
delete entry.mObj;
}
delete shutdownCleanup.mEntries;
}
}
};
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService)
.addObserver(shutdownCleanup.mObserver, "xpcom-shutdown", false /* don't hold weakly */);
delete shutdownCleanup.mEntries;
});
}
shutdownCleanup.mEntries.push({ mObj: obj, mProp: prop });
}
@ -518,7 +613,7 @@ function shutdownCleanup(obj, prop) {
// will be used to generate service accessor functions, getIOService()
function generateServiceAccessor(id, iface) {
return function this_() {
if (this_.mService === undefined) {
if (!("mService" in this_)) {
this_.mService = Components.classes[id].getService(iface);
shutdownCleanup(this_, "mService");
}

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

@ -20,6 +20,7 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Daniel Boelzle <mozilla@boelzle.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -42,6 +43,16 @@ interface calIIcalComponent;
interface calIItemBase;
interface nsIInputStream;
interface calITimezoneProvider;
interface calIIcsParser;
/**
* Listener being called once asynchronous parsing is done.
*/
[scriptable, uuid(d22527da-b0e2-41b7-b6f4-ee9c243cd285)]
interface calIIcsParsingListener : nsISupports
{
void onParsingComplete(in nsresult rc, in calIIcsParser parser);
};
/**
* An interface for parsing an ics string or stream into its items.
@ -57,12 +68,16 @@ interface calIIcsParser : nsISupports
*
* @param aICSString
* The ICS string to parse
* @param aTzProvider
* @param optional aTzProvider
* The timezone provider used to resolve timezones not contained in the
* parent VCALENDAR or null (falls back to timezone service)
* @param optional aAsyncParsing
* If non-null, parsing will be performed on a worker thread,
* and the passed listener is called when it's done
*/
void parseString(in AString aICSString,
in calITimezoneProvider aTzProvider);
[optional] in calITimezoneProvider aTzProvider,
[optional] in calIIcsParsingListener aAsyncParsing);
/**
* Parse an input stream.
@ -70,12 +85,16 @@ interface calIIcsParser : nsISupports
* @see parseString
* @param aICSString
* The stream to parse
* @param aTzProvider
* @param optional aTzProvider
* The timezone provider used to resolve timezones not contained in the
* parent VCALENDAR or null (falls back to timezone service)
* @param optional aAsyncParsing
* If non-null, parsing will be performed on a worker thread,
* and the passed listener is called when it's done
*/
void parseFromStream(in nsIInputStream aStream,
in calITimezoneProvider aTzProvider);
[optional] in calITimezoneProvider aTzProvider,
[optional] in calIIcsParsingListener aAsyncParsing);
/**
* Get the items that were in the string or stream. In case an item represents a

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

@ -38,7 +38,6 @@
* ***** END LICENSE BLOCK ***** */
#include "nsStringStream.h"
#include "nsComponentManagerUtils.h"
#include "nsIClassInfoImpl.h"
#include "calICSService.h"
#include "calTimezone.h"
@ -815,7 +814,7 @@ calIcalComponent::Get##Attrname(calIDuration **dtp) \
return NS_OK; \
}
NS_IMPL_ISUPPORTS1(calIcalComponent, calIIcalComponent)
NS_IMPL_THREADSAFE_ISUPPORTS1(calIcalComponent, calIIcalComponent)
NS_IMETHODIMP_(icalcomponent *)
calIcalComponent::GetIcalComponent()
@ -1197,7 +1196,9 @@ calIcalComponent::AddProperty(calIIcalProperty * prop)
// return NS_OK;
// }
NS_IMPL_ISUPPORTS1_CI(calICSService, calIICSService)
NS_IMPL_THREADSAFE_ISUPPORTS2(calICSService, calIICSService, nsIClassInfo)
NS_IMPL_CI_INTERFACE_GETTER1(calICSService, calIICSService)
NS_IMPL_THREADSAFE_CI(calICSService)
calICSService::calICSService()
{

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

@ -49,12 +49,14 @@ extern "C" {
}
class calICSService : public calIICSService,
public nsIClassInfo,
public cal::XpcomBase
{
public:
calICSService();
NS_DECL_ISUPPORTS
NS_DECL_NSICLASSINFO
NS_DECL_CALIICSSERVICE
};

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

@ -19,6 +19,7 @@
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* Daniel Boelzle <mozilla@boelzle.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -45,19 +46,35 @@ function calIcsParser() {
this.mProperties = new Array();
}
calIcsParser.prototype.QueryInterface =
function QueryInterface(aIID) {
if (!aIID.equals(Components.interfaces.nsISupports) &&
!aIID.equals(Components.interfaces.calIIcsParser)) {
throw Components.results.NS_ERROR_NO_INTERFACE;
}
var gIcsParserClassInfo = {
getInterfaces: function (count) {
const ifaces = [
Components.interfaces.nsISupports,
Components.interfaces.calIIcsParser,
Components.interfaces.nsIClassInfo
];
count.value = ifaces.length;
return ifaces;
},
return this;
getHelperForLanguage: function (language) {
return null;
},
contractID: "@mozilla.org/calendar/ics-parser;1",
classDescription: "Calendar ICS Parser",
classID: Components.ID("{6fe88047-75b6-4874-80e8-5f5800f14984}"),
implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
flags: Components.interfaces.nsIClassInfo.THREADSAFE
};
calIcsParser.prototype.parseString =
function ip_parseString(aICSString, aTzProvider) {
let rootComp = getIcsService().parseICS(aICSString, aTzProvider);
calIcsParser.prototype.QueryInterface =
function ip_QueryInterface(aIID) {
return doQueryInterface(this, calIcsParser.prototype, aIID, null, gIcsParserClassInfo);
};
calIcsParser.prototype.processIcalComponent =
function ip_processIcalComponent(rootComp) {
let calComp;
// libical returns the vcalendar component if there is just one vcalendar.
// If there are multiple vcalendars, it returns an xroot component, with
@ -125,6 +142,7 @@ function ip_parseString(aICSString, aTzProvider) {
break;
default:
this.mComponents.push(subComp);
break;
}
if (item) {
@ -190,42 +208,79 @@ function ip_parseString(aICSString, aTzProvider) {
notifier.showAlertNotification("", title, text, false, null, null, title);
}
break;
}
}
};
calIcsParser.prototype.parseString =
function ip_parseString(aICSString, aTzProvider, aAsyncParsing) {
if (aAsyncParsing) {
let this_ = this;
let rootComp = null;
// Do the actual ical parsing on a thread, but process the parsed ical
// components on main/UI thread.
cal.execWorker(
function parseString_worker(responseThread) {
rootComp = cal.getIcsService().parseICS(aICSString, aTzProvider);
},
function parseString_done(exc) {
this_.processIcalComponent(rootComp);
aAsyncParsing.onParsingComplete(exc ? exc.result : Components.results.NS_OK, this_);
});
} else {
this.processIcalComponent(cal.getIcsService().parseICS(aICSString, aTzProvider));
}
};
calIcsParser.prototype.parseFromStream =
function ip_parseFromStream(aStream, aTzProvider) {
// Read in the string. Note that it isn't a real string at this point,
// because likely, the file is utf8. The multibyte chars show up as multiple
// 'chars' in this string. So call it an array of octets for now.
var octetArray = [];
var binaryIS = Components.classes["@mozilla.org/binaryinputstream;1"]
.createInstance(Components.interfaces.nsIBinaryInputStream);
binaryIS.setInputStream(aStream);
octetArray = binaryIS.readByteArray(binaryIS.available());
// Some other apps (most notably, sunbird 0.2) happily splits an UTF8
// character between the octets, and adds a newline and space between them,
// for ICS folding. Unfold manually before parsing the file as utf8.This is
// UTF8 safe, because octets with the first bit 0 are always one-octet
// characters. So the space or the newline never can be part of a multi-byte
// char.
for (var i = octetArray.length - 2; i >= 0; i--) {
if (octetArray[i] == "\n" && octetArray[i+1] == " ") {
octetArray = octetArray.splice(i, 2);
function ip_parseFromStream(aStream, aTzProvider, aAsyncParsing) {
function readString(aStream_) {
// Read in the string. Note that it isn't a real string at this point,
// because likely, the file is utf8. The multibyte chars show up as multiple
// 'chars' in this string. So call it an array of octets for now.
let octetArray = [];
let binaryIS = Components.classes["@mozilla.org/binaryinputstream;1"]
.createInstance(Components.interfaces.nsIBinaryInputStream);
binaryIS.setInputStream(aStream);
octetArray = binaryIS.readByteArray(binaryIS.available());
// Some other apps (most notably, sunbird 0.2) happily splits an UTF8
// character between the octets, and adds a newline and space between them,
// for ICS folding. Unfold manually before parsing the file as utf8.This is
// UTF8 safe, because octets with the first bit 0 are always one-octet
// characters. So the space or the newline never can be part of a multi-byte
// char.
for (var i = octetArray.length - 2; i >= 0; i--) {
if (octetArray[i] == "\n" && octetArray[i+1] == " ") {
octetArray = octetArray.splice(i, 2);
}
}
// Interpret the byte-array as a UTF8-string, and convert into a
// javascript string.
let unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
// ICS files are always UTF8
unicodeConverter.charset = "UTF-8";
return unicodeConverter.convertFromByteArray(octetArray, octetArray.length);
}
// Interpret the byte-array as a UTF8-string, and convert into a
// javascript string.
var unicodeConverter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
// ICS files are always UTF8
unicodeConverter.charset = "UTF-8";
var str = unicodeConverter.convertFromByteArray(octetArray, octetArray.length);
return this.parseString(str, aTzProvider);
if (aAsyncParsing) {
let this_ = this;
let rootComp = null;
// Do the actual string reading and ical parsing on a athread, but process the parsed ical
// components on main/UI thread.
cal.execWorker(
function parseString_worker(responseThread) {
rootComp = cal.getIcsService().parseICS(readString(aStream), aTzProvider);
},
function parseString_done(exc) {
this_.processIcalComponent(rootComp);
aAsyncParsing.onParsingComplete(exc ? exc.result : Components.results.NS_OK, this_);
});
} else {
this.processIcalComponent(cal.getIcsService().parseICS(readString(aStream), aTzProvider));
}
}
calIcsParser.prototype.getItems =

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

@ -1,4 +1,3 @@
/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
@ -22,6 +21,7 @@
* Contributor(s):
* Vladimir Vukicevic <vladimir.vukicevic@oracle.com>
* Philipp Kewisch <mozilla@kewis.ch>
* Daniel Boelzle <mozilla@boelzle.org>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
@ -52,8 +52,6 @@
#include "calBaseCID.h"
static NS_DEFINE_CID(kCalICSService, CAL_ICSSERVICE_CID);
extern "C" {
#include "ical.h"
}

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

@ -55,6 +55,8 @@
#include "calIICSService.h"
#include "nsIConsoleService.h"
#include "nsServiceManagerUtils.h"
#include "nsIClassInfoImpl.h"
#include "nsIProgrammingLanguage.h"
#include "nsCOMPtr.h"
#include "calBaseCID.h"

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

@ -208,7 +208,7 @@ ltnMimeConverter.prototype = {
convertToHTML: function lmcCTH(contentType, data) {
let parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Components.interfaces.calIIcsParser);
parser.parseString(data, null);
parser.parseString(data);
let event = null;
for each (var item in parser.getItems({})) {
if (isEvent(item)) {

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

@ -138,5 +138,8 @@ pref("calendar.categories.names", "");
pref("mousewheel.withnokey.action", 0);
pref("mousewheel.withshiftkey.action", 0);
// Disable use of worker threads. Restart needed.
pref("calendar.threading.disabled", false);
// Set up user agent
#expand pref("general.useragent.extra.lightning","Lightning/__LIGHTNING_VERSION__");

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

@ -1133,7 +1133,7 @@ calDavCalendar.prototype = {
var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Components.interfaces.calIIcsParser);
try {
parser.parseString(calData, null);
parser.parseString(calData);
} catch (e) {
// Warn and continue.
// TODO As soon as we have activity manager integration,

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

@ -234,32 +234,34 @@ calICSCalendar.prototype = {
// Wrap parsing in a try block. Will ignore errors. That's a good thing
// for non-existing or empty files, but not good for invalid files.
// That's why we put them in readOnly mode
try {
var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"].
createInstance(Components.interfaces.calIIcsParser);
parser.parseString(str, null);
var items = parser.getItems({});
for each (var item in items) {
this.mMemoryCalendar.adoptItem(item, null);
let parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Components.interfaces.calIIcsParser);
let this_ = this;
let listener = { // calIIcsParsingListener
onParsingComplete: function ics_onParsingComplete(rc, parser_) {
try {
for each (let item in parser_.getItems({})) {
this_.mMemoryCalendar.adoptItem(item, null);
}
this_.unmappedComponents = parser_.getComponents({});
this_.unmappedProperties = parser_.getProperties({});
} catch (exc) {
cal.LOG("Parsing the file failed: " + exc);
this_.mObserver.onError(this_.superCalendar, exc.result, exc.toString());
this_.mObserver.onError(this_.superCalendar, calIErrors.READ_FAILED, "");
}
this_.mObserver.onEndBatch();
this_.mObserver.onLoad(this_);
// Now that all items have been stuffed into the memory calendar
// we should add ourselves as observer. It is important that this
// happens *after* the calls to adoptItem in the above loop to prevent
// the views from being notified.
this_.mMemoryCalendar.addObserver(this_.mObserver);
this_.unlock();
}
this.unmappedComponents = parser.getComponents({});
this.unmappedProperties = parser.getProperties({});
} catch(e) {
LOG("Parsing the file failed:"+e);
this.mObserver.onError(this.superCalendar, e.result, e.toString());
this.mObserver.onError(this.superCalendar, calIErrors.READ_FAILED, "");
}
this.mObserver.onEndBatch();
this.mObserver.onLoad(this);
// Now that all items have been stuffed into the memory calendar
// we should add ourselves as observer. It is important that this
// happens *after* the calls to adoptItem in the above loop to prevent
// the views from being notified.
this.mMemoryCalendar.addObserver(this.mObserver);
this.unlock();
};
parser.parseString(str, null, listener);
},
writeICS: function () {

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

@ -71,6 +71,9 @@ pref("calendar.view.visiblehours", 9);
pref("calendar.weeks.inview", 4);
pref("calendar.previousweeks.inview", 0);
// Disable use of worker threads. Restart needed.
pref("calendar.threading.disabled", false);
// default transparency of allday items; could be switched to e.g. "OPAQUE":
pref("calendar.allday.defaultTransparency", "TRANSPARENT");

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

@ -71,7 +71,7 @@ function createEventFromIcalString(icalString) {
if (/^BEGIN:VCALENDAR/.test(icalString)) {
var parser = Components.classes["@mozilla.org/calendar/ics-parser;1"]
.createInstance(Components.interfaces.calIIcsParser);
parser.parseString(icalString, null);
parser.parseString(icalString);
var items = parser.getItems({});
ASSERT(items.length == 1);
return items[0];