bug 385740: make the handler service store possible applications when storing a handler info object; r=biesi, sr=dmose
authormyk@mozilla.org
Tue, 14 Aug 2007 13:55:06 -0700
changeset 4643 ef94956c154c8758d1de52bd79f07e91f230c2a3
parent 4642 a9da2d9d01fa1624dde6e36db5815dfaec0c3da1
child 4644 20d0ff5542112195a7dd79bb25099c02ba30e674
push idunknown
push userunknown
push dateunknown
reviewersbiesi, dmose
bugs385740
milestone1.9a8pre
bug 385740: make the handler service store possible applications when storing a handler info object; r=biesi, sr=dmose
uriloader/exthandler/nsHandlerService.js
uriloader/exthandler/tests/Makefile.in
uriloader/exthandler/tests/unit/test_handlerService.js
--- a/uriloader/exthandler/nsHandlerService.js
+++ b/uriloader/exthandler/nsHandlerService.js
@@ -65,18 +65,19 @@ const NC_HANDLER_INFO       = NC_NS + "h
 // nsIHandlerInfo::preferredAction
 const NC_SAVE_TO_DISK       = NC_NS + "saveToDisk";
 const NC_HANDLE_INTERNALLY  = NC_NS + "handleInternal";
 const NC_USE_SYSTEM_DEFAULT = NC_NS + "useSystemDefault";
 
 // nsIHandlerInfo::alwaysAskBeforeHandling
 const NC_ALWAYS_ASK         = NC_NS + "alwaysAsk";
 
-// references nsIHandlerApp record
+// references nsIHandlerApp records
 const NC_PREFERRED_APP      = NC_NS + "externalApplication";
+const NC_POSSIBLE_APP       = NC_NS + "possibleApplication";
 
 // handler app ("handler") properties
 
 // nsIHandlerApp::name
 const NC_PRETTY_NAME        = NC_NS + "prettyName";
 
 // nsILocalHandlerApp::executable
 const NC_PATH               = NC_NS + "path";
@@ -115,16 +116,17 @@ HandlerService.prototype = {
     // FIXME: when we switch from RDF to something with transactions (like
     // SQLite), enclose the following changes in a transaction so they all
     // get rolled back if any of them fail and we don't leave the datastore
     // in an inconsistent state.
 
     this._ensureRecordsForType(aHandlerInfo);
     this._storePreferredAction(aHandlerInfo);
     this._storePreferredHandler(aHandlerInfo);
+    this._storePossibleHandlers(aHandlerInfo);
     this._storeAlwaysAsk(aHandlerInfo);
 
     // Write the changes to the database immediately so we don't lose them
     // if the application crashes.
     if (this._ds instanceof Ci.nsIRDFRemoteDataSource)
       this._ds.Flush();
   },
 
@@ -159,77 +161,149 @@ HandlerService.prototype = {
   // Storage Methods
 
   _storePreferredAction: function HS__storePreferredAction(aHandlerInfo) {
     var infoID = this._getInfoID(aHandlerInfo);
 
     switch(aHandlerInfo.preferredAction) {
       case Ci.nsIHandlerInfo.saveToDisk:
         this._setLiteral(infoID, NC_SAVE_TO_DISK, "true");
-        this._removeValue(infoID, NC_HANDLE_INTERNALLY);
-        this._removeValue(infoID, NC_USE_SYSTEM_DEFAULT);
+        this._removeTarget(infoID, NC_HANDLE_INTERNALLY);
+        this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT);
         break;
 
       case Ci.nsIHandlerInfo.handleInternally:
         this._setLiteral(infoID, NC_HANDLE_INTERNALLY, "true");
-        this._removeValue(infoID, NC_SAVE_TO_DISK);
-        this._removeValue(infoID, NC_USE_SYSTEM_DEFAULT);
+        this._removeTarget(infoID, NC_SAVE_TO_DISK);
+        this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT);
         break;
 
       case Ci.nsIHandlerInfo.useSystemDefault:
         this._setLiteral(infoID, NC_USE_SYSTEM_DEFAULT, "true");
-        this._removeValue(infoID, NC_SAVE_TO_DISK);
-        this._removeValue(infoID, NC_HANDLE_INTERNALLY);
+        this._removeTarget(infoID, NC_SAVE_TO_DISK);
+        this._removeTarget(infoID, NC_HANDLE_INTERNALLY);
         break;
 
       // This value is indicated in the datastore either by the absence of
       // the three properties or by setting them all "false".  Of these two
       // options, the former seems preferable, because it reduces the size
       // of the RDF file and thus the amount of stuff we have to parse.
       case Ci.nsIHandlerInfo.useHelperApp:
       default:
-        this._removeValue(infoID, NC_SAVE_TO_DISK);
-        this._removeValue(infoID, NC_HANDLE_INTERNALLY);
-        this._removeValue(infoID, NC_USE_SYSTEM_DEFAULT);
+        this._removeTarget(infoID, NC_SAVE_TO_DISK);
+        this._removeTarget(infoID, NC_HANDLE_INTERNALLY);
+        this._removeTarget(infoID, NC_USE_SYSTEM_DEFAULT);
         break;
     }
   },
 
   _storePreferredHandler: function HS__storePreferredHandler(aHandlerInfo) {
     var infoID = this._getInfoID(aHandlerInfo);
     var handlerID = this._getPreferredHandlerID(aHandlerInfo);
     var handler = aHandlerInfo.preferredApplicationHandler;
 
     if (handler) {
-      // First add a record for the preferred app to the datasource.  In the
-      // process we also need to remove any vestiges of an existing record, so
-      // we remove any properties that we aren't overwriting.
-      this._setLiteral(handlerID, NC_PRETTY_NAME, handler.name);
-      if (handler instanceof Ci.nsILocalHandlerApp) {
-        this._setLiteral(handlerID, NC_PATH, handler.executable.path);
-        this._removeValue(handlerID, NC_URI_TEMPLATE);
-      }
-      else {
-        handler.QueryInterface(Ci.nsIWebHandlerApp);
-        this._setLiteral(handlerID, NC_URI_TEMPLATE, handler.uriTemplate);
-        this._removeValue(handlerID, NC_PATH);
-      }
+      this._storeHandlerApp(handlerID, handler);
 
-      // Finally, make this app be the preferred app for the handler info.
-      // Note: at least some code completely ignores this setting and assumes
-      // the preferred app is the one whose URI follows the appropriate pattern.
+      // Make this app be the preferred app for the handler info.
+      //
+      // Note: nsExternalHelperAppService::FillContentHandlerProperties ignores
+      // this setting and instead identifies the preferred app as the resource
+      // whose URI follows the pattern urn:<class>:externalApplication:<type>.
+      // But the old downloadactions.js code used to set this property, so just
+      // in case there is still some code somewhere that relies on its presence,
+      // we set it here.
       this._setResource(infoID, NC_PREFERRED_APP, handlerID);
     }
     else {
       // There isn't a preferred handler.  Remove the existing record for it,
       // if any.
-      this._removeValue(handlerID, NC_PRETTY_NAME);
-      this._removeValue(handlerID, NC_PATH);
-      this._removeValue(handlerID, NC_URI_TEMPLATE);
-      this._removeValue(infoID, NC_PREFERRED_APP);
+      this._removeTarget(infoID, NC_PREFERRED_APP);
+      this._removeAssertions(handlerID);
+    }
+  },
+
+  /**
+   * Store the list of possible handler apps for the content type represented
+   * by the given handler info object.
+   *
+   * @param aHandlerInfo  {nsIHandlerInfo}  the handler info object
+   */
+  _storePossibleHandlers: function HS__storePossibleHandlers(aHandlerInfo) {
+    var infoID = this._getInfoID(aHandlerInfo);
+
+    // First, retrieve the set of handler apps currently stored for the type,
+    // keeping track of their IDs in a hash that we'll use to determine which
+    // ones are no longer valid and should be removed.
+    var currentHandlerApps = {};
+    var currentHandlerTargets = this._getTargets(infoID, NC_POSSIBLE_APP);
+    while (currentHandlerTargets.hasMoreElements()) {
+      let handlerApp = currentHandlerTargets.getNext();
+      if (handlerApp instanceof Ci.nsIRDFResource) {
+        let handlerAppID = handlerApp.Value;
+        currentHandlerApps[handlerAppID] = true;
+      }
+    }
+
+    // Next, store any new handler apps.
+    var newHandlerApps =
+      aHandlerInfo.possibleApplicationHandlers.enumerate();
+    while (newHandlerApps.hasMoreElements()) {
+      let handlerApp =
+        newHandlerApps.getNext().QueryInterface(Ci.nsIHandlerApp);
+      let handlerAppID = this._getPossibleHandlerAppID(handlerApp);
+      if (!this._hasResourceTarget(infoID, NC_POSSIBLE_APP, handlerAppID)) {
+        this._storeHandlerApp(handlerAppID, handlerApp);
+        this._addResourceTarget(infoID, NC_POSSIBLE_APP, handlerAppID);
+      }
+      delete currentHandlerApps[handlerAppID];
+    }
+
+    // Finally, remove any old handler apps that aren't being used anymore,
+    // and if those handler apps aren't being used by any other type either,
+    // then completely remove their record from the datastore so we don't
+    // leave it clogged up with information about handler apps we don't care
+    // about anymore.
+    for (let handlerAppID in currentHandlerApps) {
+      this._removeTarget(infoID, NC_POSSIBLE_APP, handlerAppID);
+      if (!this._existsResourceTarget(NC_POSSIBLE_APP, handlerAppID))
+        this._removeAssertions(handlerAppID);
+    }
+  },
+
+  /**
+   * Store the given handler app.
+   *
+   * Note: the reason this method takes the ID of the handler app in a param
+   * is that the ID is different than it usually is when the handler app
+   * in question is a preferred handler app, so this method can't just derive
+   * the ID of the handler app by calling _getPossibleHandlerAppID, its callers
+   * have to do that for it.
+   *
+   * @param aHandlerAppID {string}        the ID of the handler app to store
+   * @param aHandlerApp   {nsIHandlerApp} the handler app to store
+   */
+  _storeHandlerApp: function HS__storeHandlerApp(aHandlerAppID, aHandlerApp) {
+    aHandlerApp.QueryInterface(Ci.nsIHandlerApp);
+    this._setLiteral(aHandlerAppID, NC_PRETTY_NAME, aHandlerApp.name);
+
+    // In the case of the preferred handler, the handler ID could have been
+    // used to refer to a different kind of handler in the past (i.e. either
+    // a local hander or a web handler), so if the new handler is a local
+    // handler, then we remove any web handler properties and vice versa.
+    // This is unnecessary but harmless for possible handlers.
+
+    if (aHandlerApp instanceof Ci.nsILocalHandlerApp) {
+      this._setLiteral(aHandlerAppID, NC_PATH, aHandlerApp.executable.path);
+      this._removeTarget(aHandlerAppID, NC_URI_TEMPLATE);
+    }
+    else {
+      aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp);
+      this._setLiteral(aHandlerAppID, NC_URI_TEMPLATE, aHandlerApp.uriTemplate);
+      this._removeTarget(aHandlerAppID, NC_PATH);
     }
   },
 
   _storeAlwaysAsk: function HS__storeAlwaysAsk(aHandlerInfo) {
     var infoID = this._getInfoID(aHandlerInfo);
     this._setLiteral(infoID,
                      NC_ALWAYS_ASK,
                      aHandlerInfo.alwaysAskBeforeHandling ? "true" : "false");
@@ -372,16 +446,40 @@ HandlerService.prototype = {
    * @returns {string} the ID
    */
   _getPreferredHandlerID: function HS__getPreferredHandlerID(aHandlerInfo) {
     return "urn:" + this._getClass(aHandlerInfo) + ":externalApplication:" +
            aHandlerInfo.type;
   },
 
   /**
+   * Return the unique identifier for a handler app record, which stores
+   * information about a possible handler for one or more content types,
+   * including its human-readable name and the path to its executable (for a
+   * local app) or its URI template (for a web app).
+   *
+   * Note: handler app IDs for preferred handlers are different.  For those,
+   * see the _getPreferredHandlerID method.
+   *
+   * @param aHandlerApp  {nsIHandlerApp}   the handler app object
+   */
+  _getPossibleHandlerAppID: function HS__getPossibleHandlerAppID(aHandlerApp) {
+    var handlerAppID = "urn:handler:";
+
+    if (aHandlerApp instanceof Ci.nsILocalHandlerApp)
+      handlerAppID += "local:" + aHandlerApp.executable.path;
+    else {
+      aHandlerApp.QueryInterface(Ci.nsIWebHandlerApp);
+      handlerAppID += "web:" + aHandlerApp.uriTemplate;
+    }
+
+    return handlerAppID;
+  },
+
+  /**
    * Get the list of types for the given class, creating the list if it doesn't
    * already exist. The class can be either CLASS_MIMEINFO or CLASS_PROTOCOLINFO
    * (i.e. the result of a call to _getClass).
    * 
    * |urn:<class>s|
    * |urn:<class>s:root|
    * 
    * @param aClass {string} the class for which to retrieve a list of types
@@ -518,70 +616,147 @@ HandlerService.prototype = {
 
     if (target instanceof Ci.nsIRDFLiteral)
       return target.Value;
 
     return null;
   },
 
   /**
+   * Get all targets for the property of an RDF source.
+   *
+   * @param sourceURI   {string} the URI of the source
+   * @param propertyURI {string} the URI of the property
+   * 
+   * @returns {nsISimpleEnumerator} an enumerator of targets
+   */
+  _getTargets: function HS__getTargets(sourceURI, propertyURI) {
+    var source = this._rdf.GetResource(sourceURI);
+    var property = this._rdf.GetResource(propertyURI);
+
+    return this._ds.GetTargets(source, property, true);
+  },
+
+  /**
    * Set a property of an RDF source to a literal value.
    *
    * @param sourceURI   {string} the URI of the source
    * @param propertyURI {string} the URI of the property
    * @param value       {string} the literal value
    */
   _setLiteral: function HS__setLiteral(sourceURI, propertyURI, value) {
     var source = this._rdf.GetResource(sourceURI);
     var property = this._rdf.GetResource(propertyURI);
     var target = this._rdf.GetLiteral(value);
     
     this._setTarget(source, property, target);
   },
 
   /**
-   * Set a property of an RDF source to a resource.
+   * Set a property of an RDF source to a resource target.
    *
    * @param sourceURI   {string} the URI of the source
    * @param propertyURI {string} the URI of the property
-   * @param resourceURI {string} the URI of the resource
+   * @param targetURI   {string} the URI of the target
    */
-  _setResource: function HS__setResource(sourceURI, propertyURI, resourceURI) {
+  _setResource: function HS__setResource(sourceURI, propertyURI, targetURI) {
     var source = this._rdf.GetResource(sourceURI);
     var property = this._rdf.GetResource(propertyURI);
-    var target = this._rdf.GetResource(resourceURI);
+    var target = this._rdf.GetResource(targetURI);
     
     this._setTarget(source, property, target);
   },
 
   /**
    * Assert an arc into the RDF datasource if there is no arc with the given
    * source and property; otherwise, if there is already an existing arc,
-   * change it to point to the given target.
+   * change it to point to the given target. _setLiteral and _setResource
+   * call this after converting their string arguments into resources
+   * and literals, and most callers should call one of those two methods
+   * instead of this one.
    *
    * @param source    {nsIRDFResource}  the source
    * @param property  {nsIRDFResource}  the property
-   * @param value     {nsIRDFNode}      the target
+   * @param target    {nsIRDFNode}      the target
    */
   _setTarget: function HS__setTarget(source, property, target) {
     if (this._ds.hasArcOut(source, property)) {
       var oldTarget = this._ds.GetTarget(source, property, true);
       this._ds.Change(source, property, oldTarget, target);
     }
     else
       this._ds.Assert(source, property, target, true);
   },
 
   /**
+   * Assert that a property of an RDF source has a resource target.
+   * 
+   * The difference between this method and _setResource is that this one adds
+   * an assertion even if one already exists, which allows its callers to make
+   * sets of assertions (i.e. to set a property to multiple targets).
+   *
+   * @param sourceURI   {string} the URI of the source
+   * @param propertyURI {string} the URI of the property
+   * @param targetURI   {string} the URI of the target
+   */
+  _addResourceTarget: function HS__addResourceTarget(sourceURI, propertyURI,
+                                                     targetURI) {
+    var source = this._rdf.GetResource(sourceURI);
+    var property = this._rdf.GetResource(propertyURI);
+    var target = this._rdf.GetResource(targetURI);
+    
+    this._ds.Assert(source, property, target, true);
+  },
+
+  /**
+   * Whether or not a property of an RDF source has a given resource target.
+   * 
+   * The difference between this method and _setResource is that this one adds
+   * an assertion even if one already exists, which allows its callers to make
+   * sets of assertions (i.e. to set a property to multiple targets).
+   *
+   * @param sourceURI   {string} the URI of the source
+   * @param propertyURI {string} the URI of the property
+   * @param targetURI   {string} the URI of the target
+   *
+   * @returns {boolean} whether or not there is such an assertion
+   */
+  _hasResourceTarget: function HS__hasResourceTarget(sourceURI, propertyURI,
+                                                     targetURI) {
+    var source = this._rdf.GetResource(sourceURI);
+    var property = this._rdf.GetResource(propertyURI);
+    var target = this._rdf.GetResource(targetURI);
+
+    return this._ds.HasAssertion(source, property, target, true);
+  },
+
+  /**
+   * Whether or not there is an RDF source that has the given property set to
+   * the given resource target.
+   * 
+   * @param propertyURI {string} the URI of the property
+   * @param targetURI   {string} the URI of the target
+   *
+   * @returns {boolean} whether or not there is a source
+   */
+  _existsResourceTarget: function HS__existsResourceTarget(propertyURI,
+                                                           targetURI) {
+    var property = this._rdf.GetResource(propertyURI);
+    var target = this._rdf.GetResource(targetURI);
+
+    return this._ds.hasArcIn(target, property);
+  },
+
+  /**
    * Remove a property of an RDF source.
    *
    * @param sourceURI   {string} the URI of the source
    * @param propertyURI {string} the URI of the property
    */
-  _removeValue: function HS__removeValue(sourceURI, propertyURI) {
+  _removeTarget: function HS__removeTarget(sourceURI, propertyURI) {
     var source = this._rdf.GetResource(sourceURI);
     var property = this._rdf.GetResource(propertyURI);
 
     if (this._ds.hasArcOut(source, property)) {
       var target = this._ds.GetTarget(source, property, true);
       this._ds.Unassert(source, property, target);
     }
   },
--- a/uriloader/exthandler/tests/Makefile.in
+++ b/uriloader/exthandler/tests/Makefile.in
@@ -30,20 +30,20 @@
 # use your version of this file under the terms of the MPL, indicate your
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
-DEPTH		= ../../..
-topsrcdir	= @top_srcdir@
-srcdir		= @srcdir@
-VPATH		= @srcdir@
+DEPTH           = ../../..
+topsrcdir       = @top_srcdir@
+srcdir          = @srcdir@
+VPATH           = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
-MODULE		= test_uriloader_exthandler
+MODULE          = test_uriloader_exthandler
 
-XPCSHELL_TESTS = unit
+XPCSHELL_TESTS  = unit
 
 include $(topsrcdir)/config/rules.mk
--- a/uriloader/exthandler/tests/unit/test_handlerService.js
+++ b/uriloader/exthandler/tests/unit/test_handlerService.js
@@ -30,48 +30,77 @@
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 function run_test() {
+  //**************************************************************************//
+  // Constants
+
+  const handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].
+                     getService(Ci.nsIHandlerService);
+
+  const mimeSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
+                  getService(Ci.nsIMIMEService);
+
+
+  //**************************************************************************//
+  // Sample Data
+
   // It doesn't matter whether or not this nsIFile is actually executable,
   // only that it has a path and exists.  Since we don't know any executable
   // that exists on all platforms (except possibly the application being
   // tested, but there doesn't seem to be a way to get a reference to that
   // from the directory service), we use the temporary directory itself.
   var executable = HandlerServiceTest._dirSvc.get("TmpD", Ci.nsIFile);
+  // XXX We could, of course, create an actual executable in the directory:
+  //executable.append("localhandler");
+  //if (!executable.exists())
+  //  executable.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0755);
 
   var localHandler = {
     name: "Local Handler",
     executable: executable,
     interfaces: [Ci.nsIHandlerApp, Ci.nsILocalHandlerApp, Ci.nsISupports],
     QueryInterface: function(iid) {
       if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
         throw Cr.NS_ERROR_NO_INTERFACE;
       return this;
     }
   };
   
   var webHandler = Cc["@mozilla.org/uriloader/web-handler-app;1"].
                    createInstance(Ci.nsIWebHandlerApp);
   webHandler.name = "Web Handler";
   webHandler.uriTemplate = "http://www.example.com/?%s";
-  
-  var handlerSvc = Cc["@mozilla.org/uriloader/handler-service;1"].
-                   getService(Ci.nsIHandlerService);
-
-  var mimeSvc = Cc["@mozilla.org/uriloader/external-helper-app-service;1"].
-                getService(Ci.nsIMIMEService);
 
 
   //**************************************************************************//
-  // Default Properties
+  // Helper Functions
+
+  function checkLocalHandlersAreEquivalent(handler1, handler2) {
+    do_check_eq(handler1.name, handler2.name);
+    do_check_eq(handler1.executable.path, handler2.executable.path);
+  }
+
+  function checkWebHandlersAreEquivalent(handler1, handler2) {
+    do_check_eq(handler1.name, handler2.name);
+    do_check_eq(handler1.uriTemplate, handler2.uriTemplate);
+  }
+
+  // FIXME: these tests create and manipulate enough variables that it would
+  // make sense to move each test into its own scope so we don't run the risk
+  // of one test stomping on another's data.
+
+
+  //**************************************************************************//
+  // Test Default Properties
 
   // Get a handler info for a MIME type that neither the application nor
   // the OS knows about and make sure its properties are set to the proper
   // default values.
 
   var handlerInfo = mimeSvc.getFromTypeAndExtension("nonexistent/type", null);
 
   // Make sure it's also an nsIHandlerInfo.
@@ -92,17 +121,17 @@ function run_test() {
   do_check_eq(handlerInfo.description, "");
   do_check_eq(handlerInfo.hasDefaultHandler, false);
   do_check_eq(handlerInfo.defaultDescription, "");
 
   // FIXME: test a default protocol handler.
 
 
   //**************************************************************************//
-  // Round-Trip Data Integrity
+  // Test Round-Trip Data Integrity
 
   // Test round-trip data integrity by setting the properties of the handler
   // info object to different values, telling the handler service to store the
   // object, and then retrieving a new info object for the same type and making
   // sure its properties are identical.
 
   handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
   handlerInfo.preferredApplicationHandler = localHandler;
@@ -163,11 +192,75 @@ function run_test() {
   removePreferredHandlerInfo =
     mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null);
   removePreferredHandlerInfo.preferredApplicationHandler = null;
   handlerSvc.store(removePreferredHandlerInfo);
   removePreferredHandlerInfo =
     mimeSvc.getFromTypeAndExtension("nonexistent/rem-preferred-handler", null);
   do_check_eq(removePreferredHandlerInfo.preferredApplicationHandler, null);
 
+  // Make sure we can store and retrieve a handler info object with possible
+  // handlers.  We test both adding and removing handlers.
+
+  // Get a handler info and make sure it has no possible handlers.
+  var possibleHandlersInfo =
+    mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+  do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0);
+
+  // Store and re-retrieve the handler and make sure it still has no possible
+  // handlers.
+  handlerSvc.store(possibleHandlersInfo);
+  possibleHandlersInfo =
+    mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+  do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 0);
+
+  // Add two handlers, store the object, re-retrieve it, and make sure it has
+  // two handlers.
+  possibleHandlersInfo.possibleApplicationHandlers.appendElement(localHandler,
+                                                                 false);
+  possibleHandlersInfo.possibleApplicationHandlers.appendElement(webHandler,
+                                                                 false);
+  handlerSvc.store(possibleHandlersInfo);
+  possibleHandlersInfo =
+    mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+  do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 2);
+
+  // Figure out which is the local and which is the web handler and the index
+  // in the array of the local handler, which is the one we're going to remove
+  // to test removal of a handler.
+  var handler1 = possibleHandlersInfo.possibleApplicationHandlers.
+                 queryElementAt(0, Ci.nsIHandlerApp);
+  var handler2 = possibleHandlersInfo.possibleApplicationHandlers.
+                 queryElementAt(1, Ci.nsIHandlerApp);
+  var localPossibleHandler, webPossibleHandler, localIndex;
+  if (handler1 instanceof Ci.nsILocalHandlerApp)
+    [localPossibleHandler, webPossibleHandler, localIndex] = [handler1,
+                                                              handler2,
+                                                              0];
+  else
+    [localPossibleHandler, webPossibleHandler, localIndex] = [handler2,
+                                                              handler1,
+                                                              1];
+  localPossibleHandler.QueryInterface(Ci.nsILocalHandlerApp);
+  webPossibleHandler.QueryInterface(Ci.nsIWebHandlerApp);
+
+  // Make sure the two handlers are the ones we stored.
+  checkLocalHandlersAreEquivalent(localPossibleHandler, localHandler);
+  checkWebHandlersAreEquivalent(webPossibleHandler, webHandler);
+
+  // Remove a handler, store the object, re-retrieve it, and make sure
+  // it only has one handler.
+  possibleHandlersInfo.possibleApplicationHandlers.removeElementAt(localIndex);
+  handlerSvc.store(possibleHandlersInfo);
+  possibleHandlersInfo =
+    mimeSvc.getFromTypeAndExtension("nonexistent/possible-handlers", null);
+  do_check_eq(possibleHandlersInfo.possibleApplicationHandlers.length, 1);
+
+  // Make sure the handler is the one we didn't remove.
+  checkWebHandlersAreEquivalent(possibleHandlersInfo.
+                                  possibleApplicationHandlers.
+                                  queryElementAt(0, Ci.nsIHandlerApp).
+                                  QueryInterface(Ci.nsIWebHandlerApp),
+                                webHandler);
+
   // FIXME: test round trip integrity for a protocol.
   // FIXME: test round trip integrity for a handler info with a web handler.
 }