// vim: ts=4 sw=4 expandtab cindent foldmethod=marker
//
// BEGIN FLOCK GPL
// 
// Copyright Flock Inc. 2005-2006
// http://flock.com
// 
// This file may be used under the terms of of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
// 
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
// 
// END FLOCK GPL
//

const ENABLE_DEBUG = false // switch to turn off slow debug code for production
function DEBUG (x) { }
//function DEBUG (x) { debug ('flockFavoritesService: '+x+'\n'); }

// CONSTANTS {{{
//Interfaces/*{{{*/
const FAVORITES_CID                        = '{3606f63a-f874-42fa-8174-cc9a0dd0a712}';

const nsISupports                          = Components.interfaces.nsISupports;
const nsISupportsCString                   = Components.interfaces.nsISupportsCString;
const nsIFactory                           = Components.interfaces.nsIFactory;
const nsIProperties                        = Components.interfaces.nsIProperties;
const nsILocalFile                         = Components.interfaces.nsILocalFile;
const nsIPrefBranch                        = Components.interfaces.nsIPrefBranch;
const flockIWebService                     = Components.interfaces.flockIWebService;
const flockIFavoritesService               = Components.interfaces.flockIFavoritesService;
const flockIFavoritesWebService            = Components.interfaces.flockIFavoritesWebService;
const flockIFavoritesQueue                 = Components.interfaces.flockIFavoritesQueue;
const flockIFavoritesChange                = Components.interfaces.flockIFavoritesChange;
const flockIFavoritesChangeFavorite        = Components.interfaces.flockIFavoritesChangeFavorite;
const flockIFavoritesChangeCollection      = Components.interfaces.flockIFavoritesChangeCollection;
const flockIFavoriteProperties             = Components.interfaces.flockIFavoriteProperties;
const flockIFeedService                    = Components.interfaces.flockIFeedService;
const flockILuceneIndexer                  = Components.interfaces.flockILuceneIndexer;
const nsIClassInfo                         = Components.interfaces.nsIClassInfo;
const nsIRDFCompositeDataSource            = Components.interfaces.nsIRDFCompositeDataSource;
const nsIRDFRemoteDataSource               = Components.interfaces.nsIRDFRemoteDataSource;
const nsIRDFDataSource                     = Components.interfaces.nsIRDFDataSource;
const nsIRDFObserver                       = Components.interfaces.nsIRDFObserver;
const nsIRDFService                        = Components.interfaces.nsIRDFService;
const nsIRDFContainerUtils                 = Components.interfaces.nsIRDFContainerUtils;
const nsIRDFContainer                      = Components.interfaces.nsIRDFContainer;
const nsIRDFNode                           = Components.interfaces.nsIRDFNode;
const nsIRDFResource                       = Components.interfaces.nsIRDFResource;
const nsIRDFLiteral                        = Components.interfaces.nsIRDFLiteral;
const nsIRDFInt                            = Components.interfaces.nsIRDFInt;
const nsIRDFDate                           = Components.interfaces.nsIRDFDate;
const nsIRDFBlob                           = Components.interfaces.nsIRDFBlob;
const nsIWindowWatcher                     = Components.interfaces.nsIWindowWatcher;
const nsITimer                             = Components.interfaces.nsITimer;
const nsICategoryManager                   = Components.interfaces.nsICategoryManager;
/*}}}*/

// contract-ids/*{{{*/
const FAVORITES_CONTRACTID      = '@mozilla.org/rdf/datasource;1?name=flock-favorites';
const COMPOSITE_CONTRACTID      = '@mozilla.org/rdf/datasource;1?name=composite-datasource';
const MEMORY_DS_CONTRACTID      = '@mozilla.org/rdf/datasource;1?name=in-memory-datasource';
const RDFSERVICE_CONTRACTID     = '@mozilla.org/rdf/rdf-service;1';
const RDFCONTUTILS_CONTRACTID   = '@mozilla.org/rdf/container-utils;1';
const CONTAINER_CONTRACTID      = '@mozilla.org/rdf/container;1';
const DIRECTORY_SVC_CONTRACTID  = '@mozilla.org/file/directory_service;1';
const LOCAL_FILE_CONTRACTID     = '@mozilla.org/file/local;1';
const PREFERENCES_CONTRACTID    = '@mozilla.org/preferences-service;1';
const WINDOWWATCHER_CONTRACTID  = '@mozilla.org/embedcomp/window-watcher;1';
const TIMER_CONTRACTID          = '@mozilla.org/timer;1';
const FFU_CONTRACTID            = '@flock.com/favorites/utils;1';
const FQUEUE_CONTRACTID         = '@flock.com/favorites/queue;1';
const CATMAN_CONTRACTID         = '@mozilla.org/categorymanager;1';
const FEEDS_CONTRACTID          = '@flock.com/feed-service;1';
const LUCENE_CONTRACTID         = '@flock.com/lucene/indexer;1';
/*}}}*/

// services/*{{{*/
const RDFS = Components.classes[RDFSERVICE_CONTRACTID].getService (nsIRDFService);
const RDFCU = Components.classes[RDFCONTUTILS_CONTRACTID].getService (nsIRDFContainerUtils);
const DIRS = Components.classes[DIRECTORY_SVC_CONTRACTID].getService (nsIProperties);
const PREFS = Components.classes[PREFERENCES_CONTRACTID].getService (nsIPrefBranch);
const CATMAN = Components.classes[CATMAN_CONTRACTID].getService (nsICategoryManager);
/*}}}*/

// rdf namespaces/*{{{*/
function Namespace (ns) { return function (arg) { return RDFS.GetResource (ns+arg); } }
const W3RDF                     = Namespace ('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
const NSRDF                     = Namespace ('http://home.netscape.com/NC-rdf#');
const WBRDF                     = Namespace ('http://home.netscape.com/WEB-rdf#');
const FLRDF                     = Namespace ('http://flock.com/rdf#');
const COLRDF                    = Namespace ('urn:flock:collection:');
/*}}}*/

// rdf properties/*{{{*/
const RDF_URL                   = NSRDF ('URL');
const RDF_TAGS                  = FLRDF ('tags');
const RDF_TAG                   = FLRDF ('tag');
const RDF_WSID                  = FLRDF ('wsid');
const RDF_NAME                  = NSRDF ('Name');
const RDF_PERMISSIONS           = FLRDF ('permissions');
const RDF_TYPE                  = W3RDF ('type');
const RDF_FAVORITE              = FLRDF ('Favorite');
const RDF_COLLECTION            = FLRDF ('Collection');
const RDF_LIVEMARK              = NSRDF ('Livemark');
const RDF_FEEDURL               = NSRDF ('FeedURL');
const RDF_FEEDENABLED           = FLRDF ('feedEnabled');
const RDF_SHORTCUTURL           = NSRDF ('ShortcutURL');
const RDF_DESCRIPTION           = NSRDF ('Description');
const RDF_NSYNC                 = FLRDF ('nsync');
const RDF_LOCAL                 = FLRDF ('local');
const RDF_LASTMODIFIEDDATE      = WBRDF ('LastModifiedDate');
const RDF_LASTVISITDATE         = WBRDF ('LastVisitDate');
const RDF_ADDEDDATE             = FLRDF ('FavoriteAddDate');
const RDF_FAVORITESROOT         = FLRDF ('FavoritesRoot');
const RDF_COLLECTIONSROOT       = FLRDF ('CollectionsRoot');
const RDF_TAGSROOT              = FLRDF ('TagsRoot');
const RDF_INSTANCEOF            = W3RDF ('instanceOf');
const RDF_SEQ                   = W3RDF ('Seq');
const RDF_FEED                  = FLRDF ('feed');
const RDF_TOOLBAR               = FLRDF ('toolbar');
const RDF_STORE                 = FLRDF ('store');
const RDF_VERSION               = FLRDF ('version');
const RDF_HASFEEDS              = FLRDF ('hasFeeds');
const RDF_ISFEED                = FLRDF ('isFeed');
/*}}}*/

// constants from the idl/*{{{*/
const FIELD_WSID                = flockIFavoriteProperties.FIELD_WSID;
const FIELD_TAGS                = flockIFavoriteProperties.FIELD_TAGS;
const FIELD_PERMISSIONS         = flockIFavoriteProperties.FIELD_PERMISSIONS;
const FIELD_NAME                = flockIFavoriteProperties.FIELD_NAME;
const FIELD_URL                 = flockIFavoriteProperties.FIELD_URL;
const FIELD_SHORTCUTURL         = flockIFavoriteProperties.FIELD_SHORTCUTURL;
const FIELD_DESCRIPTION         = flockIFavoriteProperties.FIELD_DESCRIPTION;
const FIELD_LOCAL               = flockIFavoriteProperties.FIELD_LOCAL;
const FIELD_LASTVISITDATE       = flockIFavoriteProperties.FIELD_LASTVISITDATE;
const FIELD_LASTMODIFIEDDATE    = flockIFavoriteProperties.FIELD_LASTMODIFIEDDATE;
const FIELD_ADDEDDATE           = flockIFavoriteProperties.FIELD_ADDEDDATE;
const FIELD_FEED                = flockIFavoriteProperties.FIELD_FEED;
const FIELD_FAVICON             = flockIFavoriteProperties.FIELD_FAVICON;
const DIRECTION_TO_WEB_SERVICE = flockIFavoritesQueue.DIRECTION_TO_WEB_SERVICE;
const DIRECTION_FROM_WEB_SERVICE = flockIFavoritesQueue.DIRECTION_FROM_WEB_SERVICE;
/*}}}*/

// prefs/*{{{*/
const PREF_ID                   = 'flock.favorites.webservice.id';
const PREF_COPY_FAVORITES       = 'flock.favorites.copyFavorites';
const PREF_MERGE_COLLECTIONS    = 'flock.favorites.mergeCollections';
const PREF_SET_UP_DEFAULT_FAVORITES = 'flock.favorites.setUpDefaultFavorites';
const PREF_INDEXED_INITIAL_FAVORITES = 'flock.favorites.indexedInitialFavorites';
const PREF_SHARE_BY_DEFAULT     = 'flock.favorites.shareByDefault';
const PREF_PRIVATE_BY_DEFAULT   = 'flock.favorites.privateByDefault';
const PREF_FIRSTRUN_COMPLETED   = 'flock.firstrun.components.browser_startup.completed';

// deprecated:
const PREF_NAME                 = 'flock.favorites.webservice.name';
const PREF_USERNAME             = 'flock.favorites.webservice.username';
const PREF_PASSWORD             = 'flock.favorites.webservice.password';
const PREF_WEB_SERVICE_ENABLED  = 'flock.favorites.webservice.enabled';
const PREF_FAVORITES_INITIAL    = 'flock.favorites.initialSetup';
const PREF_UPLOAD_ALL           = 'flock.favorites.uploadAll';
/*}}}*/

const SYNC_PERIOD = 5*60; // sync every five minutes
//const SYNC_PERIOD = 30; // sync every thirty seconds - for debugging
const DOWNLOAD_PERIOD = 1; // every 1 second...
const DOWNLOAD_COUNT = 10; // ...try to import 10 new bookmarks
const FLUSH_PERIOD = 5; // flush after five seconds
const VALID_URL = /^(http|https|news|ftp):\/\//;
const ENABLE_PROFILING = false;
const MIN_WEBSERVICE_VERSION = 1;

const URL_AUTH_FAILED = 
        'chrome://browser/content/flock/favorites/authFailure.xul';

// }}}


function loadSubScript(spec)
{
    var loader = Components.classes['@mozilla.org/moz/jssubscript-loader;1']
        .getService(Components.interfaces.mozIJSSubScriptLoader);
    var context = {};
    loader.loadSubScript(spec, context);
    return context;
}
var flockFavoritesUtils = new(loadSubScript('chrome://browser/content/flock/favorites/flockFavoritesUtils.js').flockFavoritesUtils)();

// Functions to create RDF node objects from javascript types/*{{{*/

function rdfLiteral (s) {
    return RDFS.GetLiteral (s);
}

function rdfDate (d) {
    return RDFS.GetDateLiteral (d);
}

function rdfBoolean (b) {
    return rdfLiteral (b?"true":"false");
}

function rdfInt (i) {
    return RDFS.GetIntLiteral (i);
}
/*}}}*/

// rdf literals /*{{{*/
const RDF_TRUE                  = rdfBoolean (true);
const RDF_FALSE                 = rdfBoolean (false);
/*}}}*/

// utility functions/*{{{*/

// {{{
var Timing = function () { this.start = (new Date()).getTime(); }
Timing.prototype.seconds = function () {
    return ((new Date()).getTime() - this.start) / 1000;
}

// }}}

// a toString method for RDF nodes/*{{{*/
function rdf_repr (node) {
        try { 
            var value = node.QueryInterface (nsIRDFResource).ValueUTF8;
            value = value.replace ('http://www.w3.org/1999/02/22-rdf-syntax-ns#', 'W3:');
            value = value.replace ('http://home.netscape.com/NC-rdf#', 'NS:');
            value = value.replace ('http://developer.mozilla.org/rdf/vocabulary/forward-proxy#', 'FP:');
            value = value.replace ('http://flock.com/rdf#', 'FL:');
            return value;
        }
        catch (e) { }

        try { 
            var date = node.QueryInterface (nsIRDFDate);
            return 'date:'+date.Value;
        }
        catch (e) { }

        try { 
            var blob = node.QueryInterface (nsIRDFBlob);
            return 'blob: length'+blob.Length;
        }
        catch (e) { }

        try { 
            var int = node.QueryInterface (nsIRDFInt);
            return 'int:'+int.Value;
        }
        catch (e) { }

        try { 
            var literal = node.QueryInterface (nsIRDFLiteral);
            return '"'+literal.Value+'"';
        }
        catch (e) { }
        
        return 'unknown:'+node;
}/*}}}*/

// debug rdf/*{{{*/
function debug_rdf (data_source) {
    var ser=Components.classes["@mozilla.org/rdf/xml-serializer;1"].createInstance(Components.interfaces.nsIRDFXMLSerializer);
    ser.init(data_source);
    ser.QueryInterface(Components.interfaces.nsIRDFXMLSource).Serialize(
    {
        close : function(){DEBUG("");},
        flush : function(){},
        write : function (buffer,count){ DEBUG (buffer); return count; },
        writeFrom : function (stream,count){},
        isNonBlocking: false
    });
}/*}}}*/

// copy all the assertions from one datasource to another/*{{{*/
function copy_ds (ds, ds2) {
    var resources = ds.GetAllResources ();
    while (resources.hasMoreElements ()) {
        var resource = resources.getNext ();
        //DEBUG ('resource: ' + rdf_repr (resource));
        var arcs = ds.ArcLabelsOut (resource);
        while (arcs.hasMoreElements ()) {
            var arc = arcs.getNext ().QueryInterface (nsIRDFResource);
            var targets1 = ds.GetTargets (resource, arc, true);
            while (targets1.hasMoreElements ()) {
                var target = targets1.getNext().QueryInterface (nsIRDFNode);
                //DEBUG (' arc1: '+rdf_repr (arc)+' -> '+rdf_repr (target));
                ds2.Assert (resource, arc, target, true);
            }
            var targets0 = ds.GetTargets (resource, arc, false);
            while (targets0.hasMoreElements ()) {
                var target = targets0.getNext().QueryInterface (nsIRDFNode);
                //DEBUG (' arc0: '+rdf_repr (arc)+' -> '+rdf_repr (target));
                ds2.Assert (resource, arc, target, false);
            }
        }
    }
}/*}}}*/

// generate a graphviz input file/*{{{*/
function rdf_dot (ds) {
    var resources = ds.GetAllResources ();
    DEBUG ('digraph G {\n');
    while (resources.hasMoreElements ()) {
        var resource = resources.getNext ();
        var arcs = ds.ArcLabelsOut (resource);
        while (arcs.hasMoreElements ()) {
            var arc = arcs.getNext ().QueryInterface (nsIRDFResource);
            var targets1 = ds.GetTargets (resource, arc, true);
            while (targets1.hasMoreElements ()) {
                var target = targets1.getNext().QueryInterface (nsIRDFNode);
                //DEBUG ('\t"'+rdf_repr (arc)+'" '+rdf_repr (target)+'\n');
                DEBUG ('\t"'+rdf_repr (resource)+'" -> "'+rdf_repr (target)+'" [label="'+rdf_repr (arc)+'"]\n');
            }
        }
    }
    DEBUG ('}\n');
}/*}}}*//*}}}*/

// nsISimpleEnumerator implementation {{{
function simpleEnumerator (aArray) {
    aArray.hasMoreElements = function () { 
        return this.length != 0; 
    }
    aArray.getNext = function () { 
        return this.shift (); 
    }
    return aArray;
} // }}}

// Service implementation
function flockFavoritesService()/*{{{*/
{
    DEBUG ('CONSTRUCTOR: flockFavoritesService');

    if (ENABLE_PROFILING) {
        DEBUG ('enabling profiling');
        var jsds = Components.classes['@mozilla.org/js/jsd/debugger-service;1'].getService(Components.interfaces.jsdIDebuggerService);
        jsds.on();
        jsds.flags |= Components.interfaces.jsdIDebuggerService.COLLECT_PROFILE_DATA;
    }

    this.externalListeners = new Array();

    this.luceneIndexer = null; // we'll create this later...
    
    // create a datasource in the profile directory
    try {
        var profd = DIRS.get('ProfD', nsILocalFile).path;
        DEBUG ('profile dir=\"'+profd+'\"\n');
        var file = Components.classes[LOCAL_FILE_CONTRACTID]
            .createInstance(nsILocalFile);
        file.initWithPath(profd);
        file.append('flock_favorites.rdf');

        this.dataSource=RDFS.GetDataSourceBlocking ('file://' + file.path)
            .QueryInterface (nsIRDFRemoteDataSource);
    } catch (e) {
        DEBUG ('EXCEPTION: '+e);
        // or if we can't, create it in /tmp
        this.dataSource=
            RDFS.GetDataSourceBlocking ('file:///tmp/flock_favorites.rdf')
            .QueryInterface (nsIRDFRemoteDataSource);
    }

    // get references to our favorites root and collections root
    this.favoritesRoot = this.Seq (RDF_FAVORITESROOT);
    this.collectionsRoot = this.Seq (RDF_COLLECTIONSROOT);

    // migrate away from TagsRoot tag collections schema.
    if (this.isSeq (RDF_TAGSROOT)) {
        var tagsRoot = this.Seq (RDF_TAGSROOT);
        var elements = tagsRoot.GetElements ();
        while (elements.hasMoreElements ()) {
            var element = elements.getNext ();
            // FIXME: getProperties should take a mask
            var properties = this.getProperties (element);
            if (properties.fields & FIELD_TAGS) {
                this._setupTags (element, properties.tags);
            }
            tagsRoot.RemoveElement (element, true);
        }
    }

   /* disabling hasFeeds
    { // update hasFeeds for all collections
        var collections = this.collectionsRoot.GetElements ();
        while (collections.hasMoreElements ()) {
            var collection = collections.getNext ();
            this._updateHasFeeds (collection.QueryInterface (nsIRDFResource));
        }
    }
    */

    // need an url property on all favorites
    {
        var elements = this.favoritesRoot.GetElements ();
        while (elements.hasMoreElements ()) {
            var element = elements.getNext ();
            element = element.QueryInterface (nsIRDFResource);
            var literal = RDFS.GetLiteral (element.ValueUTF8);
            if (!this.dataSource.HasAssertion (element, RDF_URL, literal, true)) {
                this.dataSource.Assert (element, RDF_URL, literal, true);
            }
        }
    }


    this.webServices = new Array ();
    // find available favorites web services
    DEBUG ('Looking for web service components');
    var e = CATMAN.enumerateCategory ('flock-favorites-web-service');
    while (e.hasMoreElements ()) {
        try {
            var entry = e.getNext().QueryInterface (nsISupportsCString);
            if (!entry) { continue; }
            DEBUG ('found: '+entry.data);
            var svc = Components.classes[
                CATMAN.getCategoryEntry ('flock-favorites-web-service', 
                        entry.data)].getService (flockIFavoritesWebService);
            if (svc.version < MIN_WEBSERVICE_VERSION) {
                continue;
            }
            this.webServices.push (svc);
        } catch (e) { 
        }
    }
    DEBUG ('Found '+this.webServices.length+' favorites web services');

    this.queue = Components.classes[FQUEUE_CONTRACTID]
        .createInstance (flockIFavoritesQueue);
    this.queue.init ('default');

    // lets migrate prefs if needed
    if (PREFS.prefHasUserValue (PREF_WEB_SERVICE_ENABLED)) {
        // we're upgrading from 0.5
        if (PREFS.getBoolPref (PREF_WEB_SERVICE_ENABLED)) {
            // sharing was enabled
            if (!PREFS.prefHasUserValue (PREF_ID)) {
                // shadows used to be the default, so:
                // if no id was set, set shadows
                PREFS.setCharPref (PREF_ID, 'shadows');
            }

            PREFS.setBoolPref (PREF_PRIVATE_BY_DEFAULT,
                    !PREFS.getBoolPref (PREF_SHARE_BY_DEFAULT));
            PREFS.setBoolPref (PREF_SHARE_BY_DEFAULT, true)
        } else {
            // sharing was disabled. we should apply that.
            PREFS.setCharPref (PREF_ID, '');
        }

        // and now we should clear the old pref
        PREFS.clearUserPref (PREF_WEB_SERVICE_ENABLED);

    }

    this.initializeWebService ();

    /* default favorites */
    try {
        favorites_initial = flock_getIntPref (PREF_FAVORITES_INITIAL);
    } catch (e) { }
        
    if (!PREFS.getBoolPref (PREF_SET_UP_DEFAULT_FAVORITES)) {

        var favs = this;

        var context = {};
        context['favorite'] = function (url, props) {
            DEBUG ('FAVORITE');
            props.fields = FIELD_LOCAL;
            props.local = true;
            if (props.name != undefined) {
                props.fields |= FIELD_NAME;
            }
            if (props.description != undefined) {
                props.fields |= FIELD_DESCRIPTION;
            }
            if (props.tags != undefined) {
                props.fields |= FIELD_TAGS;
            }
            if (props.favicon != undefined) {
                props.fields |= FIELD_FAVICON;
            }
            favs.createFavorite (url, props);
        }
        context['feed'] = function (fave, url, name) {
            DEBUG ('FEED');
            /*
            if (!favs.isFeedInFavorite (url, fave)) {
                favs.createFeedInFavorite (url, 
                        {name: name, fields: FIELD_NAME }, fave);
            }
            */
        }
        context['collection'] = function (name, urls) {
            DEBUG ('COLLECTION');
            favs.createCollection (name);
            for (var i=0; i<urls.length; i++) {
                favs.addFavoriteToCollection (urls[i], name);
            }
        }
        context['defaultCollection'] = function (name) {
            DEBUG ('DEFAULT COLLECTION ("'+name+'")');
            try {
                var tc = favs.getToolbarCollection ();
                DEBUG ('DEFAULT COLLECTION: tc='+tc);
                if (tc) {
                    DEBUG ('DEFAULT COLLECTION: already set');
                    return;
                }
            } catch (e) { 
                DEBUG ('DEFAULT COLLECTION: exception='+e);
            }
            favs.setToolbarCollection (favs.getCollectionResource (name));
            DEBUG ('DEFAULT COLLECTION: set...');
        }

        var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService (Components.interfaces.mozIJSSubScriptLoader);
        loader.loadSubScript ('chrome://flock/locale/favorites/defaultFavorites.js', context);
        this.Flush (); // write the faves to disk
        PREFS.setBoolPref (PREF_SET_UP_DEFAULT_FAVORITES, true); // write set the pref
    }

    // create indexer
    this.luceneIndexer = Components.classes[LUCENE_CONTRACTID]
        .getService(flockILuceneIndexer);

    this.mSyncTimer = Components.classes[TIMER_CONTRACTID]
        .createInstance (nsITimer);
    this.mSyncTimer.initWithCallback (this, SYNC_PERIOD*1000, 
            nsITimer.TYPE_REPEATING_SLACK);

    this.mDownloadTimer = Components.classes[TIMER_CONTRACTID]
        .createInstance (nsITimer);
    this.mDownloadTimer.initWithCallback (this, DOWNLOAD_PERIOD*1000, 
            nsITimer.TYPE_REPEATING_SLACK);

    this.mFlushTimer = null;

    this.mCalculateHasFeeds = false; // disable hasFeeds


    // set up a prefs observer for the webservice id
    var prefBranch2 = PREFS.QueryInterface (Components.interfaces.nsIPrefBranch2);
    prefBranch2.addObserver (PREF_ID, this, false); // FIXME: removeObserver sometime?
    prefBranch2.addObserver (PREF_FIRSTRUN_COMPLETED, this, false); // FIXME: removeObserver sometime?

    var obService = Components.classes['@mozilla.org/observer-service;1']
                              .getService(Components.interfaces['nsIObserverService']);
    obService.addObserver( {service: this, observe: function(aSubject, aTopic, aData) { this.service.Flush(); }}, 'quit-application-requested', false);

    

    DEBUG ('flock favorites data source set up');
}/*}}}*/
flockFavoritesService.prototype.initializeWebService = function () {/*{{{*/
    var id = PREFS.getCharPref (PREF_ID);

    if (this.webService) {
        // we're switching from a web service
        if (this.webService.id == id) {
            // no we're not - we're not doing anything
            return;
        }
        
        // remove the queue
        this.webService.setFavoritesQueue (null);

        this.webService = null;
    }

    for (i in this.webServices) {
        if (this.webServices[i].id == id) {
            this.webService = this.webServices[i];
        }
    }    

    if (!this.webService) {
        // failed to initialize web service
        return;
    }

    // set up the service
    this.webService.setFavoritesQueue (this.queue);

    this.synchronize ();
}/*}}}*/
flockFavoritesService.prototype.Seq = function (node) {/*{{{*/
    // make the resource a sequence if its not and return a container object
    if (!this.isSeq (node)) {
        return RDFCU.MakeSeq (this.dataSource, node);
    }
    var container = Components.classes[CONTAINER_CONTRACTID].createInstance (nsIRDFContainer);
    container.Init (this.dataSource, node);
    return container;
}/*}}}*/
flockFavoritesService.prototype.isSeq = function (node) { // {{{
    // is the node a sequence
    return RDFCU.IsSeq (this.dataSource, node);
} // }}}
flockFavoritesService.prototype._Flush = function () { // {{{
    // schedule a flush
    if (!this.mFlushTimer) {
        this.mFlushTimer = Components.classes[TIMER_CONTRACTID]
            .createInstance (nsITimer);
        this.mFlushTimer.initWithCallback (this, FLUSH_PERIOD*1000,
                nsITimer.TYPE_ONE_SHOT);
    }
} // }}}
flockFavoritesService.prototype.Flush = function () { // {{{
    if (this.mFlushTimer) {
        this.mFlushTimer.cancel ();
        this.mFlushTimer = null;
    }
    this.dataSource.Flush ();
    if (this.queue) {
        this.queue.Flush ();
    }
} // }}}
flockFavoritesService.prototype._setupTags = function (aResource, aTags) {// {{{
    // split tags string into an array
    var tags = aTags.split(/\s*,\s*/);

    // get the old tags from RDF
    var old_tags = this.dataSource.GetTargets (aResource, RDF_TAG, true);
    while (old_tags.hasMoreElements ()) {
        var old_tag = old_tags.getNext ();
        try {
            old_tag = old_tag.QueryInterface (nsIRDFLiteral);
        } catch (e) {
            // hmm, its not a literal. This is bad news.
            this.dataSource.Unassert (aResource, RDF_TAG, old_tag);
            continue;
        }
        var old_tag_value = old_tag.Value;
        var tagindex = tags.indexOf (old_tag_value);
        if (tagindex == -1) {
            // old tag is not needed anymore
            this.dataSource.Unassert (aResource, RDF_TAG, old_tag);
        } else {
            // old tag is still present. remove it from the list to add
            tags.splice (tagindex, 1);
        }
    }

    // add new tags
    for (var i=0; i<tags.length; i++) {
        this.dataSource.Assert (aResource, RDF_TAG, rdfLiteral (tags[i]), true);
    }
}// }}}
flockFavoritesService.prototype.setProperties = function (aResource, aProperties, aMask) {/*{{{*/
    if (aProperties.fields & FIELD_TAGS) {
        // if we're setting tags...
        // force them to a common format
        aProperties.tags = aProperties.tags.split (/, */g).join (', ');
    }

    var mask = flockFavoritesUtils.setProperties (this.dataSource, aResource, 
            aProperties, aMask);

    // tags...
    if (mask & FIELD_TAGS) {
        this._setupTags (aResource, aProperties.tags);
    }

    // save the data
    this._Flush ();
}/*}}}*/
flockFavoritesService.prototype.getProperties = function (aResource) {/*{{{*/
    return flockFavoritesUtils.getProperties (this.dataSource, aResource, 0);
}/*}}}*/
flockFavoritesService.prototype.removeProperties = function (aResource) {/*{{{*/
    flockFavoritesUtils.removeProperties (this.dataSource, aResource);
}/*}}}*/

flockFavoritesService.prototype._setHasFeeds = function (aResource, aValue) { //{{{
    // disable hasFeeds
    return;

    // it has feeds...
    if (!this.isCollectionResource (aResource)) {
        return;
    }
    try {
        var target = this.dataSource.GetTarget (aResource, RDF_HASFEEDS, true);
        if (target) {
            this.dataSource.Unassert (aResource, RDF_HASFEEDS, target);
        }
    } catch (e) { }
    this.dataSource.Assert (aResource, RDF_HASFEEDS, aValue?RDF_TRUE:RDF_FALSE,
           true);

} // }}}
flockFavoritesService.prototype._updateHasFeeds = function (aResource) { //{{{
    if (!this.mCalculateHasFeeds) {
        return;
    }
    // if aResource is a favorite then lets update all the containers its in
    if (this.isFavorite (aResource.ValueUTF8)) {
        var collections = this.getFavoriteCollections (aResource.ValueUTF8);
        while (collections.hasMoreElements ()) {
            this._updateHasFeeds (collections.getNext().QueryInterface(nsIRDFResource));
        }
        return;
    }
    
    // we need to check all the members of @aResource till we find one that
    // has a feed or is a feed
    if (!this.isCollectionResource (aResource)) {
        return;
    }
    var favorites = this.Seq (aResource).GetElements ();
    while (favorites.hasMoreElements ()) {
        var favorite = favorites.getNext().QueryInterface (nsIRDFResource);

        // check if the favorite is a feed
        var props = this.getProperties (favorite);
        if ((props.fields & FIELD_FEED) && props.feed) {
            this._setHasFeeds (aResource, true);
            return;
        }

        // check if the favorite has feeds
        if (this.isSeq (favorite)) {
            var seq = this.Seq (favorite);
            if (seq.GetElements ().hasMoreElements ()) {
                this._setHasFeeds (aResource, true);
                return;
            }
        }
    }

    // no feeds were found
    this._setHasFeeds (aResource, false);
} // }}}

// synchronization
flockFavoritesService.prototype._handleGetAllFavorites = function (changes) { // {{{
    DEBUG ('flockFavoritesService._handleGetAllFavorites starts');

    var server_deletions = new Array ();
    var server_additions = new Array ();

    // build a hash-table mapping url->properties for what we got from the 
    // web service
    var server_favorites = {};
    while (changes.hasMoreElements ()) {
        var change = changes.getNext ();
        DEBUG ('change = '+change);
        change = change.QueryInterface (flockIFavoritesChangeFavorite);
        //check to see if this is a delete, and if so, short cut
        if (change.isDelete) {
            server_deletions.push (change.url);
            DEBUG ('server deletion: '+change.url);
        } else {
            change.properties.fields |= FIELD_LOCAL;
            change.properties.local = false;
            server_favorites[change.url] = change.properties;
            ENABLE_DEBUG && DEBUG ('server_favorites['+change.url+'] = '+flockFavoritesUtils.propertiesToString (change.properties));
        }
    }

    // build a hash-table mapping url->properties for what have here
    var client_favorites = {}
    var elements = this.favoritesRoot.GetElements();
    while (elements.hasMoreElements ()) {
        var r = elements.getNext ().QueryInterface (nsIRDFResource);
        client_favorites[r.ValueUTF8] = this.getProperties (r);
        ENABLE_DEBUG && DEBUG ('client_favorites[' + r.ValueUTF8 + '] = ' + 
                flockFavoritesUtils.propertiesToString (
                    client_favorites[r.ValueUTF8]));
    }

    // whats on the client but not on the server
    // unless we've already handled deletions
    if (!this.webService.supportsExplicitDeletes) {
        for (var url in client_favorites) {
            if (client_favorites[url].fields & FIELD_LOCAL &&
                    client_favorites[url].local) {
                // local favorites shouldn't be treated as deleted from the server
                continue;
            }
            if (!server_favorites[url]) {
                server_deletions.push (url);
                DEBUG ('server deletion: '+url);
            }
        }
    }
    DEBUG ('flockFavoritesService._handleGetAllFavorites '+server_deletions.length+' deletions');

    // whats on the server but not on the client
    for (var url in server_favorites) {
        if (!client_favorites[url]) {
            server_additions.push (url);
            DEBUG ('server addition: '+url);
        }
    }
    DEBUG ('flockFavoritesService._handleGetAllFavorites '+server_additions.length+' additions');

    // what are the changes from the server
    var server_changes = new Array ();
    for (var url in server_favorites) {
        if (!server_favorites[url] || !client_favorites[url]) {
            // no changes to check
            continue;
        }
        if (!flockFavoritesUtils.propertiesCompare (server_favorites[url], 
                    client_favorites[url], this.webService.supportedFields)) {
            server_changes.push (url);
            if (ENABLE_DEBUG) {
                DEBUG ('found changes to: '+url);
                DEBUG (' server props: '+flockFavoritesUtils.propertiesToString (server_favorites[url]));
                DEBUG (' client props: '+flockFavoritesUtils.propertiesToString (client_favorites[url]));
            }
        }
    }

    DEBUG ('flockFavoritesService._handleGetAllFavorites '+server_changes.length+' changes');

    // queue additions
    for (var i in server_additions) {
        var url = server_additions[i];
        if (this.queue.changesPending (url)) {
            // if there are pending changes for this do nothing
            continue;
        }
        this.queue.changeFavorite (url, server_favorites[url], 
                DIRECTION_FROM_WEB_SERVICE);
    }

    // queue deletions
    for (var i in server_deletions) {
        var url = server_deletions[i];
        if (this.queue.changesPending (url)) {
            // if there are pending changes for this do nothing
            continue;
        }
        // we don't need properties for local deletes, we do it by url
        this.queue.deleteFavorite (url, null, DIRECTION_FROM_WEB_SERVICE);
    }

    // queue changes
    for (var i in server_changes) {
        var url = server_changes[i];
        if (this.queue.changesPending (url)) {
            // if there are pending changes for this do nothing
            continue;
        }
        this.queue.changeFavorite (url, server_favorites[url], 
                DIRECTION_FROM_WEB_SERVICE);
    }


    DEBUG ('flockFavoritesService._handleGetAllFavorites ends');
} // }}}
flockFavoritesService.prototype._handleGetAllCollections = function (changes) { // {{{
    DEBUG ('flockFavoritesService._handleGetAllCollections starts');

    // XXX FIXME handle the case of pending collection changes in the queue.

    // see bug #2172 - we want to merge collections when we first sign in
    // to online favorites.
    var mergeCollections = PREFS.getBoolPref (PREF_MERGE_COLLECTIONS);
    if (mergeCollections) {
        PREFS.setBoolPref (PREF_MERGE_COLLECTIONS, false);
    }

    // we need to turn the incoming collections from enumerators into
    // arrays of arrays or something

    var server_collections = {};
    while (changes.hasMoreElements ()) {
        var change = changes.getNext ();
        change = change.QueryInterface (flockIFavoritesChangeCollection);
        DEBUG ('got collection: "'+change.name+'"');
        //check to see if this is a delete, and if so, short cut
        if (change.isDelete) {
            this._removeCollection (change.name);
        } else {
            var urls = new Array (); 
            while (change.contents.hasMoreElements ()) {
                var url = change.contents.getNext ().QueryInterface (nsIRDFResource).ValueUTF8;
                urls.push (url);
                DEBUG ('got url: '+url);
            }
            server_collections[change.name] = urls;
        }
    }

    // and we need an array representation of our local collections in a 
    // similar form...

    var client_collections = {};
    var collections = this.getAllCollections ();
    while (collections.hasMoreElements ()) {
        var collection = collections.getNext ().QueryInterface (nsIRDFResource);
        var name = this.getCollectionName (collection);
        var favorites = this.getCollectionFavorites (name);
        var urls = new Array ();
        while (favorites.hasMoreElements ()) {
            urls.push (favorites.getNext().QueryInterface (nsIRDFResource).ValueUTF8);
        }
        client_collections[name] = urls;
    } 

    // first lets remove all the collections that are present locally
    // but not present on the server
    for (var name in client_collections) {
        if (typeof server_collections[name] == 'undefined') {
            if (mergeCollections) {
                // actually, lets not remove it, lets add it to the server...
                this._setCollectionOnWebService (name)
            } else {
                if (!this.webService.supportsExplicitDeletes) {
                    // delete the collection
                    DEBUG ('removing collection: '+name);
                    this._removeCollection (name);
                }
            }
        }
    }

    // now lets add/update all the collections from the server...
    for (var name in server_collections) {
        if (typeof client_collections[name] == 'undefined') {
            // this is a new collection
            DEBUG ('got new collection: '+name);
            this._createCollection (name);
            for (var i=0; i<server_collections[name].length; i++) {
                var url = server_collections[name][i];
                if (!this.isFavorite (url)) {
                    // we need to create a favorite - the details will be
                    // filled in later when we sync down the favorite data
                    this._updateFavorite (url, {fields: 0});
                }
                this._addFavoriteToCollection (url, name);
            }
        } else {
            // this collection already exists. if the collection is the same
            // then we don't have to do anything
            var equal = true;
            if (client_collections[name].length != server_collections[name]) {
                equal = false;
            } else { 
                for (var i=0; i<server_collections[name].length; i++) {
                    if (server_collection[name][i] != 
                            client_collections[name][i]) {
                        // one of the elements is not equal.
                        equal = false;
                    }
                }
            }

            if (!equal) {
                if (mergeCollections) {
                    DEBUG ('merging collection "'+name+'"');
                    // lets just add the stuff thats missing
                    for (var i=0; i<server_collections[name].length; i++) {
                        var url = server_collections[name][i];
                        DEBUG (' url: '+url);
                        if (client_collections[name].indexOf (url) == -1) {
                            DEBUG (' add it');
                            // the server url is missing from the client.
                            if (!this.isFavorite (url)) {
                                // we need to create a favorite - the details will be
                                // filled in later when we sync down the favorite data
                                this._updateFavorite (url, {fields: 0});
                            }
                            this._addFavoriteToCollection (url, name);
                        }
                    }
                    // and send it up to the server
                    this._setCollectionOnWebService (name)
                } else {
                    // we need to empty the collection
                    for (var i=0; i<client_collections[name].length; i++) {
                        this._removeFavoriteFromCollection(client_collections[name],
                                name);
                    }
                    // and fill it with the right stuff
                    for (var i=0; i<server_collections[name].length; i++) {
                        var url = server_collections[name][i]
                        if (!this.isFavorite (url)) {
                            // we need to create a favorite - the details will be
                            // filled in later when we sync down the favorite data
                            this._updateFavorite (url, {fields: 0});
                        }
                        this._addFavoriteToCollection (url, name);
                    }
                }
            }
        }
    }

    DEBUG ('flockFavoritesService._handleGetAllCollections ends');
} // }}}
flockFavoritesService.prototype.shouldWeSynchronize = function () { //{{{
    if (!this.webService) return;

    var svc = this;
    var listener = {}
    listener.onFailed = function (message) {
        DEBUG ('flockFavoritesService.shouldWeSynchronize.listener.onFailed("'+message+'")');
    }
    listener.onSucceeded = function (aSyncRequired) {
        DEBUG ('flockFavoritesService.shouldWeSynchronize.listener.onSucceeded('+aSyncRequired+')');
        if (aSyncRequired) {
            svc.synchronize ();
        }
    }
    this.webService.getSyncRequired (listener);
}//}}}

// the nsIObserver implementation/*{{{*/
flockFavoritesService.prototype.observe = function (subject, topic, data) {
    if (topic == 'nsPref:changed' && data == PREF_ID) {
        // the pref for webservice changed

        if (PREFS.getCharPref (PREF_ID) == '' && this.webService) {
            // sharing is being switched off
            this.webService.serviceDisabled ();
        }

        // clear the queue
        this.queue.clear ();

        this.initializeWebService ();

        if (this.webService && PREFS.getBoolPref (PREF_COPY_FAVORITES)) {
            var elements = this.favoritesRoot.GetElements ();
            while (elements.hasMoreElements ()) {
                var element = elements.getNext ().QueryInterface (nsIRDFResource);
                var props = this.getProperties (element);
                if (!(props.fields & FIELD_LOCAL) || !props.local) {
                    this.queue.changeFavorite (element.ValueUTF8, props, 
                            DIRECTION_TO_WEB_SERVICE);
                }
            }
            if (this.webService.supportsCollections) {
                elements = this.collectionsRoot.GetElements ();
                while (elements.hasMoreElements ()) {
                    var element = elements.getNext ().QueryInterface (nsIRDFResource);
                    this._setCollectionOnWebService (this.getCollectionName (element));
                }
            }
        }
    }
    if (topic == 'nsPref:changed' && data == PREF_FIRSTRUN_COMPLETED &&
            PREFS.getBoolPref (PREF_FIRSTRUN_COMPLETED)) {
        if (!PREFS.getBoolPref (PREF_INDEXED_INITIAL_FAVORITES)) {
            var index_initial_callback = {};
            index_initial_callback.svc = this;
            index_initial_callback.notify = function (timer) {
                var e = this.svc.getAllFavorites ();
                while (e.hasMoreElements ()) {
                    var u = e.getNext().QueryInterface(nsIRDFResource).ValueUTF8;
                    this.svc.luceneIndexer.AddDocument (u);
                }
       
                PREFS.setBoolPref (PREF_INDEXED_INITIAL_FAVORITES, true);
            }
            var index_initial_timer = Components.classes[TIMER_CONTRACTID]
                .createInstance (nsITimer);
            index_initial_timer.initWithCallback (index_initial_callback, 
                    100, nsITimer.TYPE_ONE_SHOT);
        }

    }
}/*}}}*/

// the nsIClassInfo implementation/*{{{*/
flockFavoritesService.prototype.flags = nsIClassInfo.SINGLETON;
flockFavoritesService.prototype.classDescription = "Flock Favorites Service";
flockFavoritesService.prototype.getInterfaces = function (count) {
    var interfaceList = [flockIFavoritesService, nsIClassInfo];
    count.value = interfaceList.length;
    return interfaceList;
}
flockFavoritesService.prototype.getHelperForLanguage = function (count) {return null;}/*}}}*/

// the nsITimerCallback implementation {{{
flockFavoritesService.prototype.notify = function (aTimer) {
    if (aTimer == this.mSyncTimer) {
        if (this.webService) {
            this.shouldWeSynchronize ();
        }
        return;
    } else if (aTimer == this.mDownloadTimer) {
        var count = DOWNLOAD_COUNT;
        while (count--) {
            var change = this.queue.nextChange (DIRECTION_FROM_WEB_SERVICE);
            if (!change) return;
            try {
                change = change.QueryInterface (flockIFavoritesChangeFavorite);
                if (change.isDelete) {
                    this._removeFavorite (change.url);
                } else {
                    this._updateFavorite (change.url, change.properties);
                }
            } catch (e) { }
            try {
                change = change.QueryInterface (flockIFavoritesChangeCollection);
                if (change.isDelete) {
                    this._removeCollection (change.name);
                } else {
                    this._setCollectionContents (change.name, change.contents);
                }
            } catch (e) { }
        }
    } else if (aTimer == this.mFlushTimer && this.mFlushTimer != null) {
        this.mFlushTimer = null;
        this.dataSource.Flush ();
        DEBUG ('flushed favorites data...');
    }
} // }}}

// the nsISupports implementation/*{{{*/
flockFavoritesService.prototype.QueryInterface =
function (iid) {
    if (!iid.equals(flockIFavoritesService) && 
        !iid.equals(nsIRDFDataSource) &&
        !iid.equals(nsITimerCallback) &&
        !iid.equals(nsIClassInfo) &&
        !iid.equals(nsIObserver) &&
        !iid.equals(nsISupports))
        throw Components.results.NS_ERROR_NO_INTERFACE;
    if (iid.equals(nsIRDFDataSource) && !this.dataSourceSetup) {
    }
    return this;
}/*}}}*/

flockFavoritesService.prototype._updateFavorite = function (aURL, aProperties) { // private and unsynced {{{
    ENABLE_DEBUG && DEBUG ('_updateFavorite ('+aURL+', '+flockFavoritesUtils.propertiesToString (aProperties)+')');
    var resource = RDFS.GetResource (aURL);
    // set the favorite type
    if (!this.dataSource.HasAssertion (resource, RDF_TYPE, RDF_FAVORITE, true)){
        this.dataSource.Assert (resource, RDF_TYPE, RDF_FAVORITE, true);
    }
    // if this doesn't already exist
    if (this.favoritesRoot.IndexOf (resource) == -1) {
        // add this to the favorites root
        this.favoritesRoot.AppendElement (resource);
        // Set the added date, if we don't have it from the service
        if (!(aProperties.fields & FIELD_ADDEDDATE)) {
            aProperties.addedDate = (new Date()).getTime();
            aProperties.fields |= FIELD_ADDEDDATE;
        }
    }
    // update the modified date
    aProperties.lastModifiedDate = (new Date()).getTime();
    aProperties.fields |= FIELD_LASTMODIFIEDDATE;

    // if its not allowed to be synced, make this local.
    // FIXME: make this service-specific
    if (!aURL.match (VALID_URL)) {
        aProperties.fields |= FIELD_LOCAL;
        aProperties.local = true;
    }

    // set the properties
    var mask = this.setProperties (resource, aProperties);

    // FIXME: only index if indexable properties are changed, OR if this is a new fave

    // add this document to the full text indexer
    if (this.luceneIndexer) {
        this.luceneIndexer.AddDocument (aURL);
    }

    // update the hasFeeds for this favorite's collections
    /* disabling hasFeeds
    this._updateHasFeeds (resource);
    */

    return mask;
} // }}}
flockFavoritesService.prototype._removeFavorite = function (aURL) { // private and unsynced {{{
    // FIXME: fail for collections/collections?

    // we need to find the favorite
    var resource = RDFS.GetResource (aURL);

    // remove feed contents
    if (RDFCU.IsContainer (this.dataSource, resource)) {
        var container = this.Seq (resource);
        var children = container.GetElements ();
        while (children.hasMoreElements ()) {
            var child = children.getNext ();
            this.removeFeedFromFavorite (
                    child.QueryInterface (nsIRDFResource).Value, aURL);
        }
    }

    // remove favorite from collections
    var collections = this.getFavoriteCollections (aURL);
    while (collections.hasMoreElements ()) {
        this.removeFavoriteFromCollection (aURL, 
                this.getCollectionName (collections.getNext()
                    .QueryInterface(nsIRDFResource)));
    }

    // we need to work out if we should remove all of the properties of this 
    // favorite
    var clear_properties = true;

    var properties = this.getProperties (resource);
    if ((properties.fields & FIELD_FEED) && (properties.feed)) {
        // okay, this is a feed, lets see if its a sub-feed of any other faves
        var faves = this.favoritesRoot.GetElements ();
        while (faves.hasMoreElements ()) {
            var fave = faves.getNext ().QueryInterface (nsIRDFResource);
            if (!this.isSeq (fave)) {
                // not a sequence - not interesting
                continue;
            }
            if (this.Seq (fave).IndexOf (resource) != -1) {
                // okay, the fave we're deleting is a sub-feed of an existing
                // favorite. Lets not clear the properties
                clear_properties = false;
                break;
            }
        }
    }

    if (clear_properties) {
        // remove the properties
        this.removeProperties (resource);
    }

    // remove from "library"
    this.favoritesRoot.RemoveElement (resource, false);

    // write out the RDF
    this._Flush ();

    // try to delete this document from the full text index.
    // it will fail if the document is still in the history
    if (this.luceneIndexer && !this.luceneIndexer.TryToDelete (aURL)) {
        // we need up update the metadata in the history index
        this.luceneIndexer.AddDocument (aURL);
    }
}/*}}}*/

// the nsIRDFDataSource implementation {{{
// FIXME: can we implement this with assignment when we create the DS?
flockFavoritesService.prototype.URI = 'rdf:flock-favorites';
flockFavoritesService.prototype.GetSource = function (aProperty, aTarget, aTruthValue) { /*{{{*/
    return this.dataSource.GetSource (aProperty, aTarget, aTruthValue); 
}/*}}}*/
flockFavoritesService.prototype.GetSources = function (aProperty, aTarget, aTruthValue) { /*{{{*/
    return this.dataSource.GetSources (aProperty, aTarget, aTruthValue); 
}/*}}}*/
flockFavoritesService.prototype.GetTarget = function (aSource, aProperty, aTruthValue) { /*{{{*/
    var target = this.dataSource.GetTarget (aSource, aProperty, aTruthValue); 
    if (target == null && (aProperty.ValueUTF8 == RDF_TAGS.ValueUTF8 ||
                aProperty.ValueUTF8 == RDF_NAME.ValueUTF8 ||
                aProperty.ValueUTF8 == RDF_DESCRIPTION.ValueUTF8) ) {
        // handle missing tags, name or description
        target = rdfLiteral ('');
    }
    //debug ('flockFavoritesService.GetTargets (aSource='+aProperty.ValueUTF8+', aProperty='+aProperty.ValueUTF8+') -> '+(target?target.ValueUTF8:target)+'\n');
    return target;
}/*}}}*/
flockFavoritesService.prototype.GetTargets = function (aSource, aProperty, aTruthValue) { /*{{{*/
    return this.dataSource.GetTargets (aSource, aProperty, aTruthValue); 
}/*}}}*/
flockFavoritesService.prototype.Assert = function (aSource, aProperty, aTarget, aTruthValue) { /*{{{*/
    this.dataSource.Assert (aSource, aProperty, aTarget, aTruthValue); 
}/*}}}*/
flockFavoritesService.prototype.Unassert = function (aSource, aProperty, aTarget) { /*{{{*/
    this.dataSource.Unassert (aSource, aProperty, aTarget); 
}/*}}}*/
flockFavoritesService.prototype.Change = function (aSource, aProperty, aOldTarget, aNewTarget) { /*{{{*/
    this.dataSource.Change (aSource, aProperty, aOldTarget, aNewTarget); 
}/*}}}*/
flockFavoritesService.prototype.Move = function (aOldSource, aNewSource, aProperty, aTarget) { /*{{{*/
    this.dataSource.Move (aOldSource, aNewSource, aProperty, aTarget); 
}/*}}}*/
flockFavoritesService.prototype.HasAssertion = function (aSource, aProperty, aTarget, aTruthValue) { /*{{{*/
    return this.dataSource.HasAssertion (aSource, aProperty, aTarget, aTruthValue); 
}/*}}}*/
flockFavoritesService.prototype.AddObserver = function (aObserver) { /*{{{*/
    this.dataSource.AddObserver (aObserver); 
}/*}}}*/
flockFavoritesService.prototype.RemoveObserver = function (aObserver) { /*{{{*/
    this.dataSource.RemoveObserver (aObserver); 
}/*}}}*/
flockFavoritesService.prototype.ArcLabelsIn = function (aNode) { /*{{{*/
    return this.dataSource.ArcLabelsIn (aNode); 
}/*}}}*/
flockFavoritesService.prototype.ArcLabelsOut = function (aNode) { /*{{{*/
    return this.dataSource.ArcLabelsOut (aNode); 
}/*}}}*/
flockFavoritesService.prototype.GetAllResources = function () { /*{{{*/
    return this.dataSource.GetAllResources (); 
}/*}}}*/
flockFavoritesService.prototype.IsCommandEnabled = function (aSources, aCommand, aArguments) { /*{{{*/
    return this.dataSource.IsCommandEnabled (aSources, aCommand, aArguments); 
}/*}}}*/
flockFavoritesService.prototype.DoCommand = function (aSources, aCommand, aArguments) { /*{{{*/
    this.dataSource.DoCommand (aSources, aCommand, aArguments); 
}/*}}}*/
flockFavoritesService.prototype.GetAllCmds = function (aSource) { /*{{{*/
    return this.dataSource.GetAllCmds (aSource); 
}/*}}}*/
flockFavoritesService.prototype.hasArcIn = function (aNode, aArc) { /*{{{*/
    return this.dataSource.hasArcIn (aNode, aArc); 
}/*}}}*/
flockFavoritesService.prototype.hasArcOut = function (aNode, aArc) { /*{{{*/
    return this.dataSource.hasArcOut (aNode, aArc); 
}/*}}}*/
flockFavoritesService.prototype.beginUpdateBatch = function () { /*{{{*/
    this.dataSource.beginUpdateBatch (); 
}/*}}}*/
flockFavoritesService.prototype.endUpdateBatch = function () { /*{{{*/
    this.dataSource.endUpdateBatch (); 
}/*}}}*/
// }}}

// the flockIFavoritesService implementation
flockFavoritesService.prototype.createFavorite = function (aURL, aProperties) {/*{{{*/
    ENABLE_DEBUG && DEBUG ('createFavorite ('+aURL+', '+flockFavoritesUtils.propertiesToString (aProperties)+')');
    var mask = this._updateFavorite (aURL, aProperties);

    // send the favorite to the web service
    if (!(aProperties.fields & FIELD_LOCAL) ||
            !aProperties.local) {
        // FIXME: check change mask for syncable properties first
        this.queue.changeFavorite (aURL, aProperties, DIRECTION_TO_WEB_SERVICE);
    }
}/*}}}*/
flockFavoritesService.prototype.removeFavorite = function (aURL) {/*{{{*/
    // before we delete it, lets work out if its local
    var resource = RDFS.GetResource (aURL);
    var properties = this.getProperties (resource);
    var local = (properties.fields&FIELD_LOCAL) && properties.local;

    // remove it (unsynced)
    this._removeFavorite (aURL);

    // if its not local, queue a removal
    if (!local) {
        this.queue.deleteFavorite (aURL, properties, DIRECTION_TO_WEB_SERVICE);
    }
}/*}}}*/
flockFavoritesService.prototype.updateFavorite = function (aURL, aProperties) {/*{{{*/
    ENABLE_DEBUG && DEBUG ('updateFavorite ('+aURL+', '+flockFavoritesUtils.propertiesToString (aProperties)+')');
    var mask = this._updateFavorite (aURL, aProperties);

    var properties = this.getFavoriteProperties (aURL);

    if (!(properties.fields & FIELD_LOCAL) ||
            !properties.local) {
        // FIXME: check change mask for syncable properties first
        this.queue.changeFavorite (aURL, properties, DIRECTION_TO_WEB_SERVICE);
    }
}/*}}}*/
flockFavoritesService.prototype._isFavoriteResource = function (aResource) {/*{{{*/
    try {
        return this.dataSource.HasAssertion (aResource, RDF_TYPE, RDF_FAVORITE, true);
    } catch (e) {
        DEBUG ("couldn't get favoriteness of '"+aURL+"'");
        return false;
    }
}/*}}}*/
flockFavoritesService.prototype.isFavorite = function (aURL) {/*{{{*/
    try {
        return this._isFavoriteResource (RDFS.GetResource (aURL));
    } catch (e) {
        DEBUG ("couldn't get favoriteness of '"+aURL+"'");
        return false;
    }
}/*}}}*/
flockFavoritesService.prototype.getFavoriteProperties = function (aURL) {/*{{{*/
    var resource = RDFS.GetResource (aURL);
    /*
    if (!this.dataSource.HasAssertion (resource, RDF_TYPE, RDF_FAVORITE, true)) {
        return null;
    }
    */
    return this.getProperties (resource);
}/*}}}*/
flockFavoritesService.prototype.getAllFavorites = function () {/*{{{*/
    return this.favoritesRoot.GetElements ();
}/*}}}*/

flockFavoritesService.prototype._setCollectionOnWebService = function (aCollectionName) { // {{{
    DEBUG ('flockFavoritesService._setCollectionOnWebService ("'+aCollectionName+'")');

    if (!this.webService || !this.webService.supportsCollections) {
        // nothing to do
        return;
    }

    var collfaves = this.getCollectionFavorites (aCollectionName);
    var faves = new Array ();
    while (collfaves.hasMoreElements ()) {
        var fave = collfaves.getNext ().QueryInterface (nsIRDFResource);
        var props = this.getProperties (fave);
        if ( (props.fields&FIELD_LOCAL) && (props.local) ) {
            continue;
        }
        faves.push (fave);
    }

    this.queue.changeCollection (aCollectionName, simpleEnumerator (faves), 
            DIRECTION_TO_WEB_SERVICE);
} // }}}
flockFavoritesService.prototype._removeCollectionFromWebService = function (aCollectionName) { // {{{
    DEBUG ('flockFavoritesService._removeCollectionFromWebService ("'+aCollectionName+'")');

    if (!this.webService || !this.webService.supportsCollections) {
        // nothing to do
        return;
    }

    this.queue.deleteCollection (aCollectionName, DIRECTION_TO_WEB_SERVICE);
} // }}}
flockFavoritesService.prototype._collectionResource = function (aCollectionName) { // {{{
    DEBUG ('flockFavoritesService._collectionResource ("'+aCollectionName+'")');
    return COLRDF (escape (aCollectionName));
} // }}}
flockFavoritesService.prototype.isCollectionResource = function (aCollection) { // {{{
    return this.collectionsRoot.IndexOf (aCollection) != -1;
} // }}}
flockFavoritesService.prototype.isCollection = function (aCollectionName) { // {{{
    return this.isCollectionResource (
            this._collectionResource (aCollectionName));
} // }}}
flockFavoritesService.prototype._createCollection = function (aCollectionName) { // {{{
    var collection = this._collectionResource (aCollectionName);
    if (this.isCollectionResource (collection)) {
        // okay, this is already a collection.
        return false;
    }

    // make it a sequence
    this.Seq (collection);

    // attach the type to this collection
    this.dataSource.Assert (collection, RDF_TYPE, RDF_COLLECTION, true);

    // attach the name to this collection
    this.dataSource.Assert (collection, RDF_NAME, rdfLiteral (aCollectionName),
           true);

    // add this collection to the collections root
    this.collectionsRoot.AppendElement (collection);

    /* disable hasFeeds 
    // this is an empty collection. it has no feeds
    this._setHasFeeds (collection, false);
    */
    
    // write the changes to disk
    this._Flush ();

    return true;
} // }}}
flockFavoritesService.prototype.createCollection = function (aCollectionName) { // {{{
    var success = this._createCollection (aCollectionName);

    if (success) {
        // sync to the service
        this._setCollectionOnWebService (aCollectionName);
    }

    return success;
} // }}}

flockFavoritesService.prototype._removeCollection = function (aCollectionName) { // {{{
    DEBUG ('_removeCollection ("'+aCollectionName+'")');
    // get the resource for this collection
    var resource = this._collectionResource (aCollectionName);

    if (!this.isCollectionResource (resource)) {
        // its not a collection, how can we remove it?
        return false;
    }

    // if the collection was the toolbar, then try to make the first collection the toolbar
    if (this.dataSource.HasAssertion (resource, RDF_TOOLBAR, RDF_TRUE, true)) {
        var collections = this.collectionsRoot.GetElements ();
        if (collections.hasMoreElements ()) {
            // set the toolbar to be the first collection
            this.setToolbarCollection (collections.getNext ());
        } else {
            // set it to nothing
            this.setToolbarCollection (null);
        }
    }

    // get an nsIRDFContainer for this collection
    var seq = this.Seq (resource);

    // remove all of the contents of this collection
    var elements = seq.GetElements ();
    while (elements.hasMoreElements ()) {
        var element = elements.getNext ();
        seq.RemoveElement (element, false);
    }
    // FIXME: remove sequence metadata

    // remove type
    this.dataSource.Unassert (resource, RDF_TYPE, RDF_COLLECTION);

    // remove name
    this.dataSource.Unassert (resource, RDF_NAME, rdfLiteral (aCollectionName));
    
    // remove collection from the collectionroot
    this.collectionsRoot.RemoveElement (resource, true);

    // remove hasFeeds property
    try {
        var hasFeeds = this.dataSource.GetTarget (resource, RDF_HASFEEDS, true);
        if (hasFeeds) {
            this.dataSource.Unassert (resource, RDF_HASFEEDS, hasFeeds);
        }
    } catch (e) { }

    // flush the data to disk
    this._Flush ();
    
    return true;
} // }}}
flockFavoritesService.prototype.removeCollection = function (aCollectionName) { // {{{
    DEBUG ('removeCollection ("'+aCollectionName+'")');

    var success = this._removeCollection (aCollectionName);

    if (success) {
        // sync to the service
        this._removeCollectionFromWebService (aCollectionName);
    }

    return success;
} // }}}
flockFavoritesService.prototype._setCollectionContents = function (aCollectionName, aContents) { // {{{
    // get the resource for this collection
    var resource = this._collectionResource (aCollectionName);

    if (!this.isCollectionResource (resource)) {
        // its not a collection, lets make it one
        this._createCollection (aCollectionName);
    }

    var seq = this._Seq (resource);

    // remove everything in the collection
    var old = seq.GetElements ();
    while (old.hasMoreElements ()) {
        var fav = old.getNext ().QueryInterface (nsIRDFResource);
        this._removeFavoriteFromCollection (fav.ValueUTF8, aCollectionName);
    }

    // add all the new collections
    while (aContents.hasMoreElements ()) {
        var fav = aContents.getNext ().QueryInterface (nsIRDFResource);
        if (!this._isFavoriteResource (fav)) {
            // we don't have a favorite for this yet. It could be syncing down still
            // lets create a placeholder
            this._createFavorite (fav.ValueUTF8, 
                    {name: fav.ValueUTF8, fields: FIELD_NAME});
        }
        this._addFavoriteToCollection (fav.ValueUTF8, aCollectionName);
    }
} // }}}
flockFavoritesService.prototype.renameCollection = function (aCollectionName, aNewName) { // {{{
    /* this uses high-level calls to create a new collection and copy all the
       favorites. this is kinda lame, but kinda cool */
    if (!this.isCollection (aCollectionName) || this.isCollection (aNewName)) {
        // no can do.
        return false;
    }

    // create the new collection
    this.createCollection (aNewName);

    var old = this.getCollectionResource (aCollectionName);
    var oldseq = this.Seq (old);
    var oldpos = this.collectionsRoot.IndexOf (old);
    var nu = this.getCollectionResource (aNewName);
    var newseq = this.Seq (nu);
    var favorites = oldseq.GetElements ();
    while (favorites.hasMoreElements ()) {
        newseq.AppendElement (favorites.getNext ());
    }

    // move the new collection to where the old one was
    if (this.collectionsRoot.IndexOf (nu) != oldpos) {
        this.collectionsRoot.RemoveElement (nu, true);
        this.collectionsRoot.InsertElementAt (nu, oldpos, true);
    }

    // if the collection was the toolbar, then make it so
    if (this.dataSource.HasAssertion (old, RDF_TOOLBAR, RDF_TRUE, true)) {
        this.setToolbarCollection (nu);
    }

    /* disable hasFeeds
    // does this new collection have feeds?
    this._updateHasFeeds (nu);
    */

    this.removeCollection (aCollectionName);

    this._setCollectionOnWebService (aNewName);

    return true;
} // }}}
flockFavoritesService.prototype.getCollectionName = function (aCollection) {/*{{{*/
    if (!this.isCollectionResource (aCollection)) {
        // not a collection
        return null;
    }
    return this.dataSource.GetTarget (aCollection, RDF_NAME, true)
        .QueryInterface (nsIRDFLiteral).Value;
}/*}}}*/
flockFavoritesService.prototype.getCollectionResource = function (aCollectionName) {/*{{{*/
    var resource = this._collectionResource (aCollectionName);
    if (!this.isCollectionResource (resource)) {
        return null;
    } else {
        return resource;
    }
}/*}}}*/
flockFavoritesService.prototype._addFavoriteToCollection = function (aFavoriteURL, aCollectionName) {/*{{{*/
    // get the favorite resource
    var favorite = RDFS.GetResource (aFavoriteURL);

    // get the collection resource
    var collection = this._collectionResource (aCollectionName);
    if (!this.isCollectionResource (collection)) {
        // its not a collection
        return false;
    }

    // okay, so this is a big hack lets make this favorite a favorite
    if (this.favoritesRoot.IndexOf (favorite) == -1) {
        // its not actually a favorite. lame.
        // lets make one
        var props = this.getProperties (favorite);
        this.createFavorite (aFavoriteURL, props);
    }
    

    // get a container for the collection
    var container = this.Seq (collection);

    // if the favorite isn't already in the collection
    if (container.IndexOf (favorite) == -1) {
        // add the favorite to it
        container.AppendElement (favorite);

        /* disabling hasFeeds
        // update hasFeeds
        this._updateHasFeeds (collection);
        */

        this._Flush ();

        return true;
    }
    return false;
}/*}}}*/
flockFavoritesService.prototype.addFavoriteToCollection = function (aFavoriteURL, aCollectionName) {/*{{{*/
    DEBUG ('addFavoriteToCollection ('+aFavoriteURL+',"'+aCollectionName+'")');

    var success = this._addFavoriteToCollection (aFavoriteURL, aCollectionName);

    if (success) {
        // update it on the service
        this._setCollectionOnWebService (aCollectionName);
    }
}/*}}}*/
flockFavoritesService.prototype.orderFavoriteInCollection = function (aFavoriteURL, aCollectionName, aIndex) {/*{{{*/
    DEBUG ('orderFavoriteInCollection ('+aFavoriteURL+', "'+aCollectionName+'", ' + aIndex + ')');
    
    // get the favorite resource
    var favorite = RDFS.GetResource (aFavoriteURL);

    // get the collection resource
    var collection = this._collectionResource (aCollectionName);
    if (!this.isCollectionResource (collection)) {
        // its not a collection
        return;
    }

    // get a container for the collection
    var container = this.Seq (collection);

    // if the favorite is already in the collection, remove it
    if (container.IndexOf (favorite) != -1) {
        container.RemoveElement (favorite, true);
        //aIndex = aIndex - 1;
    }
    
    // add the new favorite
    container.InsertElementAt (favorite, aIndex + 1, true);

    this._Flush ();

    // update it on the service
    this._setCollectionOnWebService (aCollectionName);

}/*}}}*/
flockFavoritesService.prototype.getFavoriteOrderInCollection = function (aFavoriteURL, aCollectionName) { // {{{
    // get the favorite resource
    var favorite = RDFS.GetResource (aFavoriteURL);

    // get the collection resource
    var collection = this._collectionResource (aCollectionName);
    if (!this.isCollectionResource (collection)) {
        // its not a collection
        return -1;
    }

    // get a container for the collection
    var container = this.Seq (collection);

    return container.IndexOf (favorite);
} // }}}
flockFavoritesService.prototype._removeFavoriteFromCollection = function (aFavoriteURL, aCollectionName) { // {{{
    // get the favorite resource
    var favorite = RDFS.GetResource (aFavoriteURL);

    // get the collection resource
    var collection = this._collectionResource (aCollectionName);
    if (!this.isCollectionResource (collection)) {
        // its not a collection
        return false;
    }

    // get a container for the collection
    var container = this.Seq (collection);

    if (container.IndexOf (favorite) == -1) {
        // the favorite isn't in the collection
        return false;
    }

    // remove the favorite from the container
    container.RemoveElement (favorite, false);

    /* disabling hasFeeds
    // update hasFeeds
    this._updateHasFeeds (collection);
    */

    this._Flush ();

    return true;
} // }}}
flockFavoritesService.prototype.removeFavoriteFromCollection = function (aFavoriteURL, aCollectionName) { // {{{
    var success = this._removeFavoriteFromCollection (aFavoriteURL, aCollectionName);

    if (success) {
        // update it on the service
        this._setCollectionOnWebService (aCollectionName);
    }
} // }}}
flockFavoritesService.prototype.isFavoriteInCollection = function (aFavoriteURL, aCollectionName) { // {{{
    // get the favorite resource
    var favorite = RDFS.GetResource (aFavoriteURL);

    // get the collection resource
    var collection = this._collectionResource (aCollectionName);
    if (!this.isCollectionResource (collection)) {
        // its not a collection
        return;
    }

    // get a container for the collection
    var container = this.Seq (collection);

    return container.IndexOf (favorite) != -1;
} // }}}
flockFavoritesService.prototype.getCollectionFavorites = function (aCollectionName) { // {{{
    // get the collection resource
    var collection = this._collectionResource (aCollectionName);
    if (!this.isCollectionResource (collection)) {
        // its not a collection
        return;
    }

    // get a container for the collection
    var container = this.Seq (collection);

    return container.GetElements ();
} // }}}
flockFavoritesService.prototype.setToolbarCollection = function (aCollection) {/*{{{*/
    DEBUG('setToolbarCollection (' + rdf_repr(aCollection) + ')');
    var oldCollection = this.dataSource.GetSource(RDF_TOOLBAR, RDF_TRUE, true);
    if (oldCollection) {
        this.dataSource.Unassert(oldCollection, RDF_TOOLBAR, RDF_TRUE);
    }

    if (aCollection != null) { 
        this.dataSource.Assert(aCollection, RDF_TOOLBAR, RDF_TRUE, true);
    }
    this._Flush();
}/*}}}*/
flockFavoritesService.prototype.getToolbarCollection = function () {/*{{{*/
    DEBUG('getToolbarCollection ()');
    return this.dataSource.GetSource(RDF_TOOLBAR, RDF_TRUE, true);
}/*}}}*/
flockFavoritesService.prototype.getFavoriteCollections = function (aURL) { //{{{
    DEBUG ('getFavoriteCollections ('+aURL+')');
    var resource = RDFS.GetResource (aURL);
    var colls = new Array ();
    var elements = this.collectionsRoot.GetElements ();
    while (elements.hasMoreElements ()) {
        var collection = elements.getNext ().QueryInterface (nsIRDFResource);
        var seq = this.Seq (collection);
        if (seq.IndexOf (resource) != -1) {
            colls.push (collection);
        }
    }

    DEBUG ('  returning: '+colls);

    return simpleEnumerator (colls);
} // }}}
flockFavoritesService.prototype.setFavoriteCollections = function (aURL, aCollections) { //{{{
    /** whats in @aFrom but not in @aTo, compare elements using @aCompare */
    function whatsMissing (aFrom, aTo, aCompare) {
        result = []
        // whats in aFrom but not in aTo
        for (var i in aFrom) {
            var found = false;
            for (var j in aTo) {
                if (aCompare (aFrom[i], aTo[j])) {
                    found = true;
                    break;
                }
            }
            if (!found) {
                result.push (aFrom[i]);
            }
        }
        return result;
    }

    var enumerator = this.getFavoriteCollections (aURL);
    var current_collections = [];
    while (enumerator.hasMoreElements ()) {
        current_collections.push (
                this.getCollectionName (enumerator.getNext()));
    }

    enumerator = aCollections;
    var final_collections = [];
    while (enumerator.hasMoreElements ()) {
        final_collections.push (this.getCollectionName (enumerator.getNext()));
    }

    var deletions = whatsMissing (current_collections, final_collections,
            function (a, b) { return a == b; });
    var additions = whatsMissing (final_collections, current_collections,
            function (a, b) { return a == b; });

    for (var i in deletions) {
        this.removeFavoriteFromCollection (aURL, deletions[i]);
    }

    for (var i in additions) {
        this.addFavoriteToCollection (aURL, additions[i]);
    }
} // }}}
flockFavoritesService.prototype.getAllCollections = function () { //{{{
    return this.collectionsRoot.GetElements ();
} // }}}

flockFavoritesService.prototype.isFeed = function (aFeedURL) { // {{{
    return this.isFeedResource (RDFS.GetResource (aFeedURL));
} // }}}
flockFavoritesService.prototype.isFeedResource = function (aFeed) { /*{{{*/
    var properties = this.getProperties (aFeed);
    return (properties.fields&FIELD_FEED && properties.feed)
}/*}}}*/
flockFavoritesService.prototype.createFeedInFavorite = function (aFeedURL, aFeedProperties, aFavoriteURL) { // {{{
    DEBUG ('createFeedInFavorite (feed='+aFeedURL+', fave='+aFavoriteURL+')');
    // we need to find the favorite and feed
    var resource = RDFS.GetResource (aFavoriteURL);
    var feed = RDFS.GetResource (aFeedURL);
    var favorite = this.Seq(resource);

    favorite.AppendElement(feed);

    aFeedProperties.fields |= FIELD_FEED;
    aFeedProperties.feed = true;

    this.setProperties (feed, aFeedProperties);

    /* disabling hasFeeds
    // update hasFeeds
    this._updateHasFeeds (resource);
    */
} // }}}
flockFavoritesService.prototype.removeFeedFromFavorite = function (aFeedURL, aFavoriteURL) { // {{{
    
    // we need to find the favorite and feed
    var resource = RDFS.GetResource (aFavoriteURL);
    var feed = RDFS.GetResource (aFeedURL);
    var favorite = this.Seq (resource);

    favorite.RemoveElement (feed, false);

    /* disabling hasFeeds
    // update hasFeeds
    this._updateHasFeeds (resource);
    */

    // we need to work out if we should remove all of the properties of this 
    // feed
    var clear_properties = true;

    if (this.collectionsRoot.IndexOf (feed) != -1) {
        // if the feed is in fact a favorite...
        clear_properties = false;
    } else {
        var properties = this.getProperties (feed);
        // lets see if its a sub-feed of any other faves
        var faves = this.favoritesRoot.GetElements ();
        while (faves.hasMoreElements ()) {
            var fave = faves.getNext ().QueryInterface (nsIRDFResource);
            if (!this.isSeq (fave)) {
                // not a sequence - not interesting
                continue;
            }
            if (this.Seq (fave).IndexOf (feed) != -1) {
                // okay, the fave we're deleting is a sub-feed of an existing
                // favorite. Lets not clear the properties
                clear_properties = false;
                break;
            }
        }
    }

    if (clear_properties) {
        // remove the properties
        this.removeProperties (feed);
    }

    this._Flush ();
} // }}}
flockFavoritesService.prototype.isFeedInFavorite = function (aFeedURL, aFavoriteURL) { // {{{

    // we need to find the favorite and feed
    var resource = RDFS.GetResource (aFavoriteURL);
    var feed = RDFS.GetResource (aFeedURL);
    var favorite = this.Seq (resource);

    return (favorite.IndexOf (feed) != -1);
} // }}}
flockFavoritesService.prototype.getFeedsInFavorite = function (aFavoriteURL, count) { /*{{{*/
    // we need to find the favorite
    var resource = RDFS.GetResource (aFavoriteURL);

    // get all flock#feed properties for this favorite
    var arr = new Array ();
    var targets;
    if (RDFCU.IsContainer(this.dataSource, resource)) {
        DEBUG("IT *IS* A CONTAINER!");
        var cont = this.Seq (resource);
        targets = cont.GetElements();
    }
    
    //var targets = this.dataSource.GetTargets (resource, RDF_FEED, true);
    while (targets && targets.hasMoreElements ()) {
        // FIXME handle non-resource targets for this
        var item = targets.getNext();
        var itemRsrc = item.QueryInterface(nsIRDFResource);
        arr.push (itemRsrc.ValueUTF8);
    }
    count.value = arr.length;
    return arr;
}/*}}}*/
flockFavoritesService.prototype.getAllFeeds = function () { // {{{
    // we need to get all sources of ->RDF_FEED->"true"
    //var sources = this.dataSource.GetSources (RDF_ISFEED, RDF_TRUE, true);

    // or not...

    var feeds = [];
    var faves = this.favoritesRoot.GetElements ();
    while (faves.hasMoreElements ()) {
        var fave = faves.getNext ().QueryInterface (nsIRDFResource);
        if (this.isFeedResource (fave)) {
            feeds.push (fave);
        }
    }

    return simpleEnumerator (feeds);
}/*}}}*/
flockFavoritesService.prototype.getFeedProperties = function (aFeedURL) { // {{{
    var resource = RDFS.GetResource (aFeedURL);
    return this.getProperties (resource);
}/*}}}*/
flockFavoritesService.prototype.setFeedProperties = function (aFeedURL, aFeedProperties) { // {{{
    var resource = RDFS.GetResource (aFeedURL);
    if (this.favoritesRoot.IndexOf (resource) != -1) {
        // actually, its also a favorite
        this.updateFavorite (aFeedURL, aFeedProperties);
    } else {
        // nope, just a feed
        this.setProperties (resource, aFeedProperties);
    }
}/*}}}*/

flockFavoritesService.prototype.getWebServices = function () {/*{{{*/
    // time to make an nsISimpleEnumerator
    var e = this.webServices.slice (); // shallow copy
    e.QueryInterface = function (aIID) {
        if (aIID.equals(nsISimpleEnumerator) || aIID.equals(nsISupports)) {
            return this;
        } else {
            throw Components.results.NS_ERROR_NO_INTERFACE;
        }
    }
    return simpleEnumerator (e);
}/*}}}*/
flockFavoritesService.prototype.authenticationFailed = function () {/*{{{*/
    // authentication failed!
    if (PREFS.getCharPref (PREF_ID) == '') return; // hrm

    // disable sharing
    PREFS.setCharPref (PREF_ID, '');

    // and show a dialog box
    var ww = Components.classes[WINDOWWATCHER_CONTRACTID]
        .getService (nsIWindowWatcher);
    var w = ww.openWindow (null, URL_AUTH_FAILED, null, null, null);

    DEBUG ('authenticationFailed:'+w);
    
}/*}}}*/
flockFavoritesService.prototype.synchronize = function () { //{{{
    var svc = this;

    // get all favorites
    var flistener = {}
    flistener.onFailed = function (message) {
        DEBUG ('flockFavoritesService.synchronize.flistener.onFailed("'+message+'")');
    }
    flistener.onSucceeded = function (changes) {
        DEBUG ('flockFavoritesService.synchronize.flistener.onSucceeded('+changes+')');
        svc.dataSource.beginUpdateBatch();
        svc._handleGetAllFavorites (changes);

        // After we sync favs we can sync collections - bug 3148
        // XXX - there is still a race conditions:

        //     flock & shadows thinks - collection version: 60, bookmark version: 50
        //     client 1 pull favorites
        //     client 2 push new favorite in a collection
        //     client 1 pull collections
        //     flock thinks - collection version: 61, bookmark version: 50

        // Result: favorite is in library, but not in collection.
        //         it should be added after the next getAllCollections that syncs

        if (svc.webService.supportsCollections) {
            // get all collections
            var clistener = {}
            clistener.onFailed = function (message) {
                DEBUG ('flockFavoritesService.synchronize.clistener.onFailed("'+message+'")');
            }
            clistener.onSucceeded = function (changes) {
                DEBUG ('flockFavoritesService.synchronize.clistener.onSucceeded('+changes+')');
                svc._handleGetAllCollections (changes);
            }
            svc.webService.getAllCollections (clistener);
        }
        svc.dataSource.endUpdateBatch();
    }
    this.webService.getAllFavorites (flistener);

}//}}}

flockFavoritesService.prototype.addProgressListener = function (aListener) {/*{{{*/
    this.externalListeners[this.externalListeners.length] = aListener;
}/*}}}*/
flockFavoritesService.prototype.removeProgressListener = function (aListener) {/*{{{*/
    var newListeners = new Array();
    for(var i=0;i<this.externalListeners.length;++i) {
        if(this.externalListeners[i] == aListener) continue;
        newListeners[newListeners.length] = this.externalListeners[i];
    }
    this.externalListeners = newListeners;
}/*}}}*/

flockFavoritesService.prototype.disableHasFeeds = function () { // {{{
    this.mCalculateHasFeeds = false;
} // }}}
flockFavoritesService.prototype.enableHasFeeds = function () { // {{{
    /* disabling hasFeeds
    this.mCalculateHasFeeds = true;

    // for each collecxtion...
    var collections = this.getAllCollections ();
    while (collections.hasMoreElements ()) {
        var collection = collections.getNext ().QueryInterface (nsIRDFResource);
        // update the feeds...
        this._updateHasFeeds (collection);
    }
    */
} // }}}

// fields
flockFavoritesService.prototype.webService = null;
flockFavoritesService.prototype.externalListeners = null;

// string bundle/*{{{*/
var strBundleService = null;
function srGetStrBundle(path)
{
   var strBundle = null;
   if (!strBundleService) {
       try {
          strBundleService =
          Components.classes["@mozilla.org/intl/stringbundle;1"].getService(); 
          strBundleService = 
          strBundleService.QueryInterface(Components.interfaces.nsIStringBundleService);
       } catch (ex) {
          DEBUG("--** strBundleService failed: " + ex);
          return null;
      }
   }
   strBundle = strBundleService.createBundle(path); 
   if (!strBundle) {
       DEBUG("--** strBundle createInstance failed **--");
   }
   return strBundle;
}/*}}}*/


function loadLibrary(aFilename) { // {{{
    var file = Components.classes["@mozilla.org/file/directory_service;1"]
        .getService(Components.interfaces.nsIProperties)
        .get("ComsD", Components.interfaces.nsIFile);
    file.append(aFilename);

    var ios = Components.classes["@mozilla.org/network/io-service;1"]
        .getService(Components.interfaces.nsIIOService);
    var fileHandler = ios.getProtocolHandler("file")
        .QueryInterface(Components.interfaces.nsIFileProtocolHandler);
    var spec = fileHandler.getURLSpecFromFile(file);

    var scriptLoader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
        .getService(Components.interfaces.mozIJSSubScriptLoader);

    scriptLoader.loadSubScript(spec);
} // }}}

// load the XPCOM library
loadLibrary("flockXPCOMTemplate.js.lib");

var module = new xpcomModule (FAVORITES_CID, FAVORITES_CONTRACTID,
        'Flock Favorites Service',
        flockFavoritesService);
function NSGetModule(aCompMgr, aFileSpec) {
    return module;
};
