/** vim: ts=2 sw=2 expandtab
//
// BEGIN SONGBIRD GPL
//
// This file is part of the Songbird web player.
//
// Copyright(c) 2005-2008 POTI, Inc.
// http://songbirdnest.com
//
// This file may be licensed under the terms of of the
// GNU General Public License Version 2 (the "GPL").
//
// Software distributed under the License is distributed
// on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either
// express or implied. See the GPL for the specific language
// governing rights and limitations.
//
// You should have received a copy of the GPL along with this
// program. If not, go to http://www.gnu.org/licenses/gpl.html
// or write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// END SONGBIRD GPL
//
 */

/**
 * \file sbBookmarks.js
 * \brief the service pane service manages the tree behind the service pane.
 * It provides an interface for creating bookmark nodes in the service pane tree.
 *
 * TODO:
 *  - move downloading to nsIChannel and friends for better chrome-proofing
 *  - send version and locale info when requesting new bookmarks
 *  - handle errors more elegantly in bookmarks downloading
 *  - handle the adding of bookmarks before the defaults are downloaded
 *  - allow the server bookmarks list to remove stale entries [bug 2352]
 * PERHAPS:
 *  - move default bookmarks downloading to after first-run so we can have
 *  per-locale bookmarks?
 *  - download and cache images
 *  - add a confirmation dialog for for bookmark deletion
 *  
 */

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;

const CONTRACTID = "@songbirdnest.com/servicepane/bookmarks;1"
const ROOTNODE = "SB:Bookmarks"
const BOOKMARK_DRAG_TYPE = 'text/x-sb-bookmark';
const MOZ_URL_DRAG_TYPE = 'text/x-moz-url';
const BSP = 'http://songbirdnest.com/rdf/bookmarks#';
const SP='http://songbirdnest.com/rdf/servicepane#';

function SB_NewDataRemote(a,b) {
  return (new Components.Constructor("@songbirdnest.com/Songbird/DataRemote;1",
                    "sbIDataRemote", "init"))(a,b);
}

function sbBookmarks() {
  this._servicePane = null;
  this._stringBundle = null;
  
  // use the default stringbundle to translate tree nodes
  this.stringbundle = null;

  this._importAttempts = 5;
  this._importTimer = null;
  this._importRequest = null;
}
sbBookmarks.prototype.QueryInterface = 
function sbBookmarks_QueryInterface(iid) {
  if (!iid.equals(Ci.nsISupports) &&
    !iid.equals(Ci.sbIBookmarks) &&
    !iid.equals(Ci.sbIServicePaneModule)) {
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
  return this;
}
sbBookmarks.prototype.servicePaneInit = 
function sbBookmarks_servicePaneInit(sps) {
  this._servicePane = sps;
  
  Components.utils.import("resource://app/jsmodules/SBDataRemoteUtils.jsm");

  // if we don't have a bookmarks node, lets create one
  this._bookmarkNode = this._servicePane.getNode(ROOTNODE);
  if (!this._bookmarkNode) {
    // create bookmarks folder
    this._bookmarkNode = this.addFolderAt('SB:Bookmarks',
        '&servicesource.bookmarks', null, sps.root, null);
  }

  // set the weight of the bookmarks node
  this._bookmarkNode.setAttributeNS(SP, 'Weight', 4);
  
  // if a flag was set by the safe mode dialog to signify that we should
  // restore the default bookmarks, do so by first deleting all the current
  // ones, and then forcing an import like it would happen on first run.
  var restoreDefault = false;
  var prefBranch  = Components.classes["@mozilla.org/preferences-service;1"]
                              .getService(Components.interfaces.nsIPrefBranch);
  try {
    restoreDefault = prefBranch.getBoolPref("browser.bookmarks.restore_default_bookmarks");
  } catch (e) { }
  if (restoreDefault) {
    // remove all current bookmarks
    while (this._bookmarkNode.firstChild != null) {
      this._servicePane.removeNode(this._bookmarkNode.firstChild);
    }
    // forget that we did the import already
    this._bookmarkNode.setAttributeNS(BSP, 'Imported', 'false');
    // reset the safe mode flag so we don't keep doing this over and over
    prefBranch.setBoolPref("browser.bookmarks.restore_default_bookmarks", "false");
  }
    
  // if the bookmark node doesn't have the Imported attribute set, lets do an import
  if (this._bookmarkNode.getAttributeNS(BSP, 'Imported') != 'true') {
    // make sure we clean up correctly on shutdown
    var obs = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
    obs.addObserver(this, "quit-application-granted", false);

    // run the importer
    this.importBookmarks();
  }

  var sbSvc = Cc["@mozilla.org/intl/stringbundle;1"].getService(Ci.nsIStringBundleService);
  this._stringBundle = sbSvc.createBundle("chrome://songbird/locale/songbird.properties");
}

sbBookmarks.prototype.scheduleImportBookmarks =
function sbBookmarks_scheduleImportBookmarks() {
  this._importTimer = Cc['@mozilla.org/timer;1'].createInstance(Ci.nsITimer);
  this._importTimer.init(this, 5000, Ci.nsITimer.TYPE_ONE_SHOT);
}

sbBookmarks.prototype.observe = 
function sbBookmarks_observe(subject, topic, data) {
  if (topic == 'timer-callback' && subject == this._importTimer) {
    // the bookmarks import timer
    this._importTimer = null;
    if (!this._servicePane) {
      // hmm, we're shutting down, no need to import now
      return;
    }
    this.importBookmarks();
  } else if (topic == "quit-application-granted") {
    if (this._importRequest) {
      this._importRequest.abort();
      this._importRequest = null;
      // spin this (main) thread until we have no pending events, to prevent
      // a deadlock due to NSS.  See bug 12055 for details.
      var thread = Cc["@mozilla.org/thread-manager;1"]
                     .getService(Ci.nsIThreadManager)
                     .currentThread;
      while(thread.processNextEvent(false));
    }
    var obs = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
    obs.removeObserver(this, "quit-application-granted");
  }
}

sbBookmarks.prototype.importBookmarks =
function sbBookmarks_importBookmarks() {
  var prefsService =
      Cc["@mozilla.org/preferences-service;1"].
      getService(Ci.nsIPrefBranch);
  var bookmarksURL = prefsService.getCharPref("songbird.url.bookmarks");
  
  // fetch the default set of bookmarks through a series of tubes
  // FIXME: don't use XHR - use nsIChannel and friends
  // FIXME: send parameters and/or headers to indicate product version or something
  var xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
      .createInstance(Ci.nsIDOMEventTarget);
  var sps = this._servicePane;
  var service = this;
  function importError() {
    service._importRequest = null;
    service._importAttempts--;
    if (service._importAttempts < 0) {
      // we tried, but it's time to give up till the next time the player starts
      // but first, let's create some default bookmarks to get us through
      var default_bookmarks = [
        {name:'Add-ons', url:'http://addons.songbirdnest.com/'},
        {name:'Directory', url:'http://birdhouse.songbirdnest.com/directory'}];
      for (var i=0; i<default_bookmarks.length; i++) {
        var bm = default_bookmarks[i];
        var bnode = sps.getNode(bm.url);
        if (!bnode) {
          service.addBookmarkAt(bm.url, bm.name, 
            'chrome://songbird-branding/skin/logo_16.png', 
            service._bookmarkNode, null);
        }
      }
      
      return;
    }
    service.scheduleImportBookmarks();
  }
  xhr.addEventListener('load', function(evt) {
    service._importRequest = null;
    var root = null;
    try {
      // a non-XML page will cause an exception here.
      root = xhr.responseXML.documentElement;
    } catch (e) {
      // catch it and try again
      importError();
      return;
    }
    var folders = root.getElementsByTagName('folder');
    for (var f=0; folders && f<folders.length; f++) {
      var folder = folders[f];
      if (!folder.hasAttribute('id') ||
          !folder.hasAttribute('name')) {
        // the folder is missing required attributes, we must ignore it
        continue;
      }

      var fnode = sps.getNode(folder.getAttribute('id'));
      
      if (!fnode) {
        // if the folder doesn't exist, create it
        fnode = service.addFolderAt(folder.getAttribute('id'),
            folder.getAttribute('name'), folder.getAttribute('image'),
            sps.root, null);
      }
      
      fnode.isOpen = (folder.getAttribute('open') == 'true');
      
      if (fnode && fnode.getAttributeNS(BSP, 'Imported') == 'true') {
        // don't reimport a folder that's already been imported
        continue;
      }
      
      if (fnode.id == ROOTNODE) {
        // we just created the default bookmarks root
        // we'll need that later if we want to let the user create
        // their own bookmarks
        service._bookmarkNode = fnode;
      }
      
      // now, let's create what goes in
      var bookmarks = folder.getElementsByTagName('bookmark');
      for (var b=0; bookmarks && b<bookmarks.length; b++) {
        var bookmark = bookmarks[b];
        if (!bookmark.hasAttribute('url') ||
            !bookmark.hasAttribute('name')) {
          // missing required attributes
          continue;
        }
        // If the bookmark already exists, then it's somewhere else
        // in the tree and we should leave it there untouched.
        // Except that it should be marked as imported now...
        var bnode = sps.getNode(bookmark.getAttribute('url'));
        if (!bnode) {
          // create the bookmark
          bnode = service.addBookmarkAt(bookmark.getAttribute('url'),
              bookmark.getAttribute('name'), bookmark.getAttribute('image'),
              fnode, null);
        }
        // remember we imported it.
        bnode.setAttributeNS(BSP, 'Imported', 'true');
      }
      
      fnode.setAttributeNS(BSP, 'Imported', 'true');
    }
    
    // try to import json bookmarks from 0.2.5
    try {
      service.migrateLegacyBookmarks();
    } catch (e) {
    }

  }, false);
  xhr.addEventListener('error', function(evt) {
    importError();
  }, false);

  // XXXredfive - this will(may) change to the mozilla urlformatter when
  //  bmo 430235 gets fixed.
  // use the urlFormatter service to replace the %FOO% mumbo-jumbo
  var urlFormatter = Components.classes["@songbirdnest.com/moz/sburlformatter;1"]
                       .getService(Components.interfaces.sbIURLFormatter);

  var pbag = Components.classes["@mozilla.org/hash-property-bag;1"]
                       .createInstance(Components.interfaces.nsIWritablePropertyBag2);

  bookmarksURL = urlFormatter.formatURL(bookmarksURL, pbag);

  xhr.QueryInterface(Ci.nsIXMLHttpRequest);
  xhr.open('GET', bookmarksURL, true);
  this._importRequest = xhr;
  xhr.send(null);
}

sbBookmarks.prototype.shutdown = 
function sbBookmarks_shutdown() {
  this._bookmarkNode = null;
  this._servicePane = null;
  this._stringBundle = null;
  this._importAttempts = -1; // do not make any more attempts to import
  if (this._importTimer) {
    this._importTimer.cancel();
    this._importTimer = null;
  }
}

sbBookmarks.prototype.getString =
function sbBookmarks_getString(aStringId, aDefault) {
  try {
    return this._stringBundle.GetStringFromName(aStringId);
  } catch (e) {
    return aDefault;
  }
}
sbBookmarks.prototype.migrateLegacyBookmarks =
function sbBookmarks_migrateLegacyBookmarks() {
  try {
    var prefsService =
        Cc["@mozilla.org/preferences-service;1"].
        getService(Ci.nsIPrefBranch);
    var LEGACY_BOOKMARKS_PREF = 'songbird.bookmarks.serializedTree';
    if (prefsService.prefHasUserValue(LEGACY_BOOKMARKS_PREF)) {
      var json = '(' + prefsService.getCharPref(LEGACY_BOOKMARKS_PREF) + ')';
      var bms = eval(json);
      for (var i in bms.children) {
        var folder = bms.children[i];
        if (folder.label == '&servicesource.bookmarks') {
          for (var j in folder.children) {
            var bm = folder.children[j];
            if (bm.properties != 'bookmark') {
              continue;
            }
            var node = this._servicePane.getNode(bm.url);
            if (node) {
              // this bookmark already existed.
              // we want to set the title to the old
              node.name = bm.label;
            } else {
              // the bookmark does not exist. We need to create it
              var icon = null;
              // only import the icon if its from the web
              if (bm.icon.match(/^http/)) {
                icon = bm.icon;
              }
              this.addBookmark(bm.url, bm.label, icon);
            }
          }
          break;
        }
      }
      // let's clear that pref
      prefsService.clearUserPref(LEGACY_BOOKMARKS_PREF);
    }
  } catch (e) {
  }
}

sbBookmarks.prototype.addBookmark =
function sbBookmarks_addBookmark(aURL, aTitle, aIconURL) {
  dump('sbBookmarks.addBookmark('+aURL.toSource()+','+aTitle.toSource()+','+aIconURL.toSource()+')\n');
  if (!this._bookmarkNode) {
    // if we try to add a bookmark the defaults are loaded, lets create the default folder anyway
    this._bookmarkNode = this._servicePane.addNode(ROOTNODE,
        this._servicePane.root, true);
  }
  return this.addBookmarkAt(aURL, aTitle, aIconURL, this._bookmarkNode, null);
}

sbBookmarks.prototype.addBookmarkAt =
function sbBookmarks_addBookmarkAt(aURL, aTitle, aIconURL, aParent, aBefore) {
  var bnode = this._servicePane.addNode(aURL, aParent, false);
  if (!bnode) {
    return bnode;
  }
  
  SBDataSetBoolValue('browser.canbookmark', false);
  
  bnode.url = aURL;
  bnode.name = aTitle;
  if (aBefore) {
    aBefore.parentNode.insertBefore(bnode, aBefore);
  }
  bnode.properties = "bookmark " + this._makeCSSProperty(aTitle);
  bnode.hidden = false;
  bnode.contractid = CONTRACTID;
  bnode.dndDragTypes = BOOKMARK_DRAG_TYPE;
  bnode.dndAcceptNear = BOOKMARK_DRAG_TYPE;
  bnode.editable = true;
  
  if (aIconURL && aIconURL.match(/^https?:/)) {
    // check that the supplied image url works, otherwise use the default
    var checker = Cc["@mozilla.org/network/urichecker;1"]
      .createInstance(Ci.nsIURIChecker);
    var uri = Cc["@mozilla.org/network/standard-url;1"]
      .createInstance(Ci.nsIURI);
    uri.spec = aIconURL;
    checker.init(uri);
    checker.asyncCheck(new ImageUriCheckerObserver(bnode, aIconURL), null);
  }

  return bnode;
}

function ImageUriCheckerObserver(bnode, icon) {
  this._bnode = bnode;
  this._icon = icon;
}

ImageUriCheckerObserver.prototype.onStartRequest =
function ImageUriCheckerObserver_onStartRequest(aRequest, aContext)
{
}
ImageUriCheckerObserver.prototype.onStopRequest =
function ImageUriCheckerObserver_onStopRequest(aRequest, aContext, aStatusCode)
{
  if (aStatusCode == 0) {

    if(aRequest &&
       aRequest.baseChannel &&
       aRequest.baseChannel instanceof Ci.nsIHttpChannel) {

      var channel = aRequest.baseChannel.QueryInterface(Ci.nsIHttpChannel);

      if (channel) {
        try {
          var contentType = channel.getResponseHeader("content-type");
            
          if (contentType.substr(0,6) != "image/") {
            Cu.reportError("Favicon URL is not an image - content-type = " + 
                           contentType + 
                           "faviconURL = " +
                           this._icon);
            return;
          }
        }
        catch(e) {
          if (Components.lastResult != Cr.NS_ERROR_NOT_AVAILABLE) {
            Cu.reportError(e);
          }
        }
      }
    }

    // If the requested image exists, set it as the icon.
    this._bnode.image = this._icon;

  }
  
  // Otherwise, we don't set the image property and we get the default from the skin.
  return;
}

sbBookmarks.prototype.addFolder =
function sbBookmarks_addFolder(aTitle) {
  return this.addFolderAt(null, aTitle, null, this._servicePane.root, null);
}

sbBookmarks.prototype.addFolderAt =
function sbBookmarks_addFolderAt(aId, aTitle, aIconURL, aParent, aBefore) {
  var  fnode = this._servicePane.addNode(aId, aParent, true);  
  fnode.name = aTitle;
  if (aIconURL != null) {
    fnode.image = aIconURL;
  }
  if (aBefore) {
    aBefore.parentNode.insertBefore(fnode, aBefore);
  }
  fnode.properties = "folder " + this._makeCSSProperty(aTitle);
  fnode.hidden = false;
  fnode.contractid = CONTRACTID;
  fnode.dndAcceptIn = BOOKMARK_DRAG_TYPE;
  fnode.editable = false; // folder names are not editable
  
  return fnode;
}

sbBookmarks.prototype.bookmarkExists =
function sbBookmarks_bookmarkExists(aURL) {
  var node = this._servicePane.getNode(aURL);
  return (node != null);
}
sbBookmarks.prototype.fillContextMenu =
function sbBookmarks_fillContextMenu(aNode, aContextMenu, aParentWindow) {
  dump ('called fillContextMenu with node: '+aNode+'\n');

  if (!aNode.editable) {
    // not editable - don't add any items
    return;
  }
  
  if (aNode.contractid != CONTRACTID) {
    dump('or not...('+aNode.contractid+')\n');
    return;
  }
    
  var document = aContextMenu.ownerDocument;
    
  var item = document.createElement('menuitem');
  item.setAttribute('label',
            this.getString('bookmarks.menu.rename', 'Rename'));
  var service = this; // so we can get to it from the callback
  item.addEventListener('command',
  function bookmark_edit_oncommand (event) {
    var servicepane = aParentWindow.document.getElementById('servicepane');
    servicepane.startEditingNode(aNode);
  }, false);
  aContextMenu.appendChild(item);

  item = document.createElement('menuitem');
  item.setAttribute('label',
            this.getString('bookmarks.menu.edit', 'Edit'));
  var service = this; // so we can get to it from the callback
  item.addEventListener('command',
  function bookmark_edit_oncommand (event) {
    dump ('edit properties of: '+aNode.name+'\n');
    aParentWindow.QueryInterface(Ci.nsIDOMWindowInternal);
    /* begin code stolen from windowUtils.js:SBOpenModalDialog */
    /* FIXME: what's that BackscanPause stuff? */
    var chromeFeatures = "chrome,centerscreen,modal=yes,resizable=no";
    var accessibility = SB_NewDataRemote('accessibility.enabled', null).boolValue;
    chromeFeatures += (',titlebar='+accessibility?'yes':'no');
    aParentWindow.openDialog('chrome://songbird/content/xul/editBookmark.xul',
                 'edit_bookmark', chromeFeatures, aNode);
  }, false);
  aContextMenu.appendChild(item);
  
  item = document.createElement('menuitem');
  item.setAttribute('label',
            this.getString('bookmarks.menu.remove', 'Remove'));
  item.addEventListener('command',
  function bookmark_delete_oncommand (event) {
    dump ('delete: '+aNode.name+'\n');
    // FIXME: confirmation dialog, eh??
    service._servicePane.removeNode(aNode);
    
    // handle case where the currently viewed page is being removed
    if(event.view.gBrowser.currentURI.spec == aNode.id)
      SBDataSetBoolValue('browser.canbookmark', true);
  }, false);
  aContextMenu.appendChild(item);
}

sbBookmarks.prototype.fillNewItemMenu =
function sbBookmarks_fillNewItemMenu(aNode, aContextMenu, aParentWindow) {
  // XXX lone: disabled; see bug 4387
  /*
  var stringBundle = this._stringBundle;
  function add(id, label, accesskey, oncommand) {
    var menuitem = aContextMenu.ownerDocument.createElement('menuitem');
    menuitem.setAttribute('id', id);
    menuitem.setAttribute('class', 'menuitem-iconic');
    menuitem.setAttribute('label', stringBundle.GetStringFromName(label));
    menuitem.setAttribute('accesskey', stringBundle.GetStringFromName(accesskey));
    menuitem.setAttribute('oncommand', oncommand);
    aContextMenu.appendChild(menuitem);
  }

  add('file.folder', 'menu.servicepane.file.folder', 'menu.servicepane.file.folder.accesskey', 'doMenu("file.folder")');
  */
}

sbBookmarks.prototype.onSelectionChanged=
function sbBookmarks_onSelectionChanged(aNode, aContainer, aParentWindow) {
}

sbBookmarks.prototype.canDrop =
function sbBookmarks_canDrop(aNode, aDragSession, aOrientation, aWindow) {
  if (aNode.isContainer) {
    if (aOrientation != 0) {
      // for folders you can only drop on the folder, not before or after
      return false;
    }
  }
  if (aDragSession.isDataFlavorSupported(MOZ_URL_DRAG_TYPE)) {

    // Check to ensure that we're not attempting to bookmark chrome URLs.
    var dragData = this._getDragData(aDragSession, MOZ_URL_DRAG_TYPE);
    if(dragData.indexOf("chrome://") == 0) {
      return false;
    }

    return true;
  }
  return false;
}
sbBookmarks.prototype._getDragData =
function sbBookmarks__getDragData(aDragSession, aDataType) {
  // create an nsITransferable
  var transferable = Cc["@mozilla.org/widget/transferable;1"].
      createInstance(Ci.nsITransferable);
  // specify what kind of data we want it to contain
  transferable.addDataFlavor(aDataType);
  // ask the drag session to fill the transferable with that data
  aDragSession.getData(transferable, 0);
  // get the data from the transferable
  var data = {};
  var dataLength = {};
  transferable.getTransferData(aDataType, data, dataLength);
  // it's always a string. always.
  data = data.value.QueryInterface(Ci.nsISupportsString);
  return data.toString();
}
sbBookmarks.prototype.onDrop =
function sbBookmarks_onDrop(aNode, aDragSession, aOrientation, aWindow, aWindow) {
  if (aDragSession.isDataFlavorSupported(MOZ_URL_DRAG_TYPE)) {
    var data = this._getDragData(aDragSession, MOZ_URL_DRAG_TYPE).split('\n');
    var url = data[0];
    var text = data[1];
    var parent, before=null;
    if (aNode.isContainer) {
      parent = aNode;
    } else {
      parent = aNode.parentNode;
      before = aNode;
      if (aOrientation == 1) {
        // after
        before = aNode.nextSibling;
      }
    }
    this.addBookmarkAt(url, text, null, parent, before);
  }
}
sbBookmarks.prototype._addDragData =
function sbBookmarks__addDragData (aTransferable, aData, aDataType) {
  aTransferable.addDataFlavor(aDataType);
  var text = Cc["@mozilla.org/supports-string;1"].
     createInstance(Ci.nsISupportsString);
  text.data = aData;
  // double the length - it's unicode - this is stupid
  aTransferable.setTransferData(aDataType, text, text.data.length*2);
}
sbBookmarks.prototype.onDragGesture =
function sbBookmarks_onDragGesture(aNode, aTransferable) {
  if (aNode.isContainer) {
    // you can't drag folders - that should be handled at the
    // service pane service layer
    return false;
  }
  
  // attach a text/x-moz-url
  // this is for dragging to other places
  /*
  this._addDragData(aTransferable, aNode.url+'\n'+aNode.name, MOZ_URL_DRAG_TYPE);
  */
  
  // and say yet - lets do this drag
  return true;
}

/**
 * Called when the user has attempted to rename a bookmark node
 */
sbBookmarks.prototype.onRename =
function sbBookmarks_onRename(aNode, aNewName) {
  if (aNode && aNewName) {
    aNode.name = aNewName;
  }
}

/**
 * Turn a partial entity (&foo.bar) into a css property string (foo-bar),
 * but leaves other strings as they are.
 */
sbBookmarks.prototype._makeCSSProperty = 
function sbBookmarks__makeCSSProperty(aString) {
  if ( aString[0] == "&" ) {
    aString = aString.substr(1, aString.length);
    aString = aString.replace(/\./g, "-");
  }
  return aString;
}

/**
 * /brief XPCOM initialization code
 */
function makeGetModule(CONSTRUCTOR, CID, CLASSNAME, CONTRACTID, CATEGORIES) {
  return function (comMgr, fileSpec) {
    return {
      registerSelf : function (compMgr, fileSpec, location, type) {
        compMgr.QueryInterface(Ci.nsIComponentRegistrar);
        compMgr.registerFactoryLocation(CID,
                        CLASSNAME,
                        CONTRACTID,
                        fileSpec,
                        location,
                        type);
        if (CATEGORIES && CATEGORIES.length) {
          var catman =  Cc["@mozilla.org/categorymanager;1"]
              .getService(Ci.nsICategoryManager);
          for (var i=0; i<CATEGORIES.length; i++) {
            var e = CATEGORIES[i];
            catman.addCategoryEntry(e.category, e.entry, e.value, 
              true, true);
          }
        }
      },

      getClassObject : function (compMgr, cid, iid) {
        if (!cid.equals(CID)) {
          throw Cr.NS_ERROR_NO_INTERFACE;
        }

        if (!iid.equals(Ci.nsIFactory)) {
          throw Cr.NS_ERROR_NOT_IMPLEMENTED;
        }

        return this._factory;
      },

      _factory : {
        createInstance : function (outer, iid) {
          if (outer != null) {
            throw Cr.NS_ERROR_NO_AGGREGATION;
          }
          return (new CONSTRUCTOR()).QueryInterface(iid);
        }
      },

      unregisterSelf : function (compMgr, location, type) {
        compMgr.QueryInterface(Ci.nsIComponentRegistrar);
        compMgr.unregisterFactoryLocation(CID, location);
        if (CATEGORIES && CATEGORIES.length) {
          var catman =  Cc["@mozilla.org/categorymanager;1"]
              .getService(Ci.nsICategoryManager);
          for (var i=0; i<CATEGORIES.length; i++) {
            var e = CATEGORIES[i];
            catman.deleteCategoryEntry(e.category, e.entry, true);
          }
        }
      },

      canUnload : function (compMgr) {
        return true;
      },

      QueryInterface : function (iid) {
        if ( !iid.equals(Ci.nsIModule) ||
             !iid.equals(Ci.nsISupports) )
          throw Cr.NS_ERROR_NO_INTERFACE;
        return this;
      }

    };
  }
}

var NSGetModule = makeGetModule (
  sbBookmarks,
  Components.ID("{21e3e540-1191-46b5-a041-d0ab95d4c067}"),
  "Songbird Bookmarks Service",
  CONTRACTID,
  [{
    category: 'service-pane',
    entry: 'bookmarks',
    value: '@songbirdnest.com/servicepane/bookmarks;1'
  }]);


