/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * 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.
 *
 * The Original Code is Microsummarizer.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Myk Melez <myk@mozilla.org> (Original Author)
 *  Simon Bünzli <zeniko@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * 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 ***** */

const Cc = Components.classes;
const Ci = Components.interfaces;

const PERMS_FILE    = 0644;
const MODE_WRONLY   = 0x02;
const MODE_TRUNCATE = 0x20;

const NS_ERROR_MODULE_DOM = 2152923136;
const NS_ERROR_DOM_BAD_URI = NS_ERROR_MODULE_DOM + 1012;

// How often to check for microsummaries that need updating, in milliseconds.
const CHECK_INTERVAL = 15 * 1000; // 15 seconds

const MICSUM_NS = new Namespace("http://www.mozilla.org/microsummaries/0.1");
const XSLT_NS = new Namespace("http://www.w3.org/1999/XSL/Transform");

//@line 60 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
const NC_NS                   = "http://home.netscape.com/NC-rdf#";
const RDF_NS                  = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
const FIELD_RDF_TYPE          = RDF_NS + "type";
const VALUE_MICSUM_BOOKMARK   = NC_NS + "MicsumBookmark";
const VALUE_NORMAL_BOOKMARK   = NC_NS + "Bookmark";
const FIELD_MICSUM_GEN_URI    = NC_NS + "MicsumGenURI";
const FIELD_MICSUM_EXPIRATION = NC_NS + "MicsumExpiration";
const FIELD_GENERATED_TITLE   = NC_NS + "GeneratedTitle";
const FIELD_CONTENT_TYPE      = NC_NS + "ContentType";
const FIELD_BOOKMARK_URL      = NC_NS + "URL";
//@line 71 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

const MAX_SUMMARY_LENGTH = 4096;

function MicrosummaryService() {}

MicrosummaryService.prototype = {

//@line 97 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
  // RDF Service
  __rdf: null,
  get _rdf() {
    if (!this.__rdf)
      this.__rdf = Cc["@mozilla.org/rdf/rdf-service;1"].
                   getService(Ci.nsIRDFService);
    return this.__rdf;
  },

  // Bookmarks Data Source
  __bmds: null,
  get _bmds() {
    if (!this.__bmds)
      this.__bmds = this._rdf.GetDataSource("rdf:bookmarks");
    return this.__bmds;
  },

  // Old Bookmarks Service
  __bms: null,
  get _bms() {
    if (!this.__bms)
      this.__bms = this._bmds.QueryInterface(Ci.nsIBookmarksService);
    return this.__bms;
  },
//@line 122 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
 
  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  // Observer Service
  __obs: null,
  get _obs() {
    if (!this.__obs)
      this.__obs = Cc["@mozilla.org/observer-service;1"].
                   getService(Ci.nsIObserverService);
    return this.__obs;
  },

  /**
   * Make a URI from a spec.
   * @param   spec
   *          The string spec of the URI.
   * @returns An nsIURI object.
   */
  _uri: function MSS__uri(spec) {
    return this._ios.newURI(spec, null, null);
  },

//@line 152 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
  /**
   * Make an RDF resource from a URI spec.
   * @param   uriSpec
   *          The URI spec to convert into a resource.
   * @returns An nsIRDFResource object.
   */
  _resource: function MSS__resource(uriSpec) {
    return this._rdf.GetResource(uriSpec);
  },

  /**
   * Make an RDF literal from a string.
   * @param   str
   *          The string from which to construct the literal.
   * @returns An nsIRDFLiteral object
   */
  _literal: function MSS__literal(str) {
    return this._rdf.GetLiteral(str);
  },
//@line 172 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

  // Directory Locator
  __dirs: null,
  get _dirs() {
    if (!this.__dirs)
      this.__dirs = Cc["@mozilla.org/file/directory_service;1"].
                   getService(Ci.nsIProperties);
    return this.__dirs;
  },

  // The update interval as specified by the user (defaults to 30 minutes)
  get _updateInterval() {
    var updateInterval =
      getPref("browser.microsummary.updateInterval", 30);
    // the minimum update interval is 1 minute
    return Math.max(updateInterval, 1) * 60 * 1000;
  },

  // A cache of local microsummary generators.  This gets built on startup
  // by the _cacheLocalGenerators() method.
  _localGenerators: {},

  // The timer that periodically checks for microsummaries needing updating.
  _timer: null,

  // Interfaces this component implements.
  interfaces: [Ci.nsIMicrosummaryService, Ci.nsIObserver, Ci.nsISupports],

  // nsISupports

  QueryInterface: function MSS_QueryInterface(iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummaryService) &&
        !iid.equals(Ci.nsIObserver) &&
        !iid.equals(Ci.nsISupportsWeakReference) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  // nsIObserver

  observe: function MSS_observe(subject, topic, data) {
    switch (topic) {
      case "xpcom-shutdown":
        this._destroy();
        break;
    }
  },

  _init: function MSS__init() {
    this._obs.addObserver(this, "xpcom-shutdown", true);

    // Periodically update microsummaries that need updating.
    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    var callback = {
      _svc: this,
      notify: function(timer) { this._svc._updateMicrosummaries() }
    };
    this._timer.initWithCallback(callback,
                                 CHECK_INTERVAL,
                                 this._timer.TYPE_REPEATING_SLACK);

    this._cacheLocalGenerators();
  },
  
  _destroy: function MSS__destroy() {
    this._timer.cancel();
    this._timer = null;
  },

  _updateMicrosummaries: function MSS__updateMicrosummaries() {
    var bookmarks = this._getBookmarks();

    var now = Date.now();
    var updateInterval = this._updateInterval;
    for ( var i = 0; i < bookmarks.length; i++ ) {
      var bookmarkID = bookmarks[i];

      // Skip this page if its microsummary hasn't expired yet.
      if (this._hasField(bookmarkID, FIELD_MICSUM_EXPIRATION) &&
          this._getField(bookmarkID, FIELD_MICSUM_EXPIRATION) > now)
        continue;

      // Reset the expiration time immediately, so if the refresh is failing
      // we don't try it every 15 seconds, potentially overloading the server.
      this._setField(bookmarkID, FIELD_MICSUM_EXPIRATION, now + updateInterval);

      // Try to update the microsummary, but trap errors, so an update
      // that throws doesn't prevent us from updating the rest of them.
      try {
        this.refreshMicrosummary(bookmarkID);
      }
      catch(ex) {
        Components.utils.reportError(ex);
      }
    }
  },
  
  _updateMicrosummary: function MSS__updateMicrosummary(bookmarkID, microsummary) {
    this._setField(bookmarkID, FIELD_GENERATED_TITLE, microsummary.content);
    this._setField(bookmarkID, FIELD_MICSUM_EXPIRATION,
                   Date.now() + (microsummary.updateInterval || this._updateInterval));

    LOG("updated microsummary for page " + microsummary.pageURI.spec +
        " to " + microsummary.content);
  },

  /**
   * Load local generators into the cache.
   * 
   */
  _cacheLocalGenerators: function MSS__cacheLocalGenerators() {
    // Load generators from the application directory.
    var appDir = this._dirs.get("MicsumGens", Ci.nsIFile);
    if (appDir.exists())
      this._cacheLocalGeneratorDir(appDir);

    // Load generators from the user's profile.
    var profileDir = this._dirs.get("UsrMicsumGens", Ci.nsIFile);
    if (profileDir.exists())
      this._cacheLocalGeneratorDir(profileDir);
  },

  /**
   * Load local generators from a directory into the cache.
   *
   * @param   dir
   *          nsIFile object pointing to directory containing generator files
   * 
   */
  _cacheLocalGeneratorDir: function MSS__cacheLocalGeneratorDir(dir) {
    var files = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
    var file = files.nextFile;

    while (file) {
      // Recursively load generators so support packs containing
      // lots of generators can organize them into multiple directories.
      if (file.isDirectory())
        this._cacheLocalGeneratorDir(file);
      else
        this._cacheLocalGeneratorFile(file);

      file = files.nextFile;
    }
    files.close();
  },

  /**
   * Load a local generator from a file into the cache.
   * 
   * @param   file
   *          nsIFile object pointing to file from which to load generator
   * 
   */
  _cacheLocalGeneratorFile: function MSS__cacheLocalGeneratorFile(file) {
    var uri = this._ios.newFileURI(file);

    var t = this;
    var callback =
      function MSS_cacheLocalGeneratorCallback(resource) {
        try     { t._handleLocalGenerator(resource) }
        finally { resource.destroy() }
      };

    var resource = new MicrosummaryResource(uri);
    resource.load(callback);
  },

  _handleLocalGenerator: function MSS__handleLocalGenerator(resource) {
    if (!resource.isXML)
      throw(resource.uri.spec + " microsummary generator loaded, but not XML");

    // Fix the generator's ID if it was installed before we started using URNs
    // to uniquely identify generators.
    // XXX This code can go away after Fx2 beta2, when enough users will have
    // upgraded from earlier versions to make bug 346822 no longer significant.
    this._fixGeneratorID(resource.content, resource.uri);

    var generator = new MicrosummaryGenerator();
    generator.localURI = resource.uri;
    generator.initFromXML(resource.content);

    // Add the generator to the local generators cache.
    // XXX Figure out why Firefox crashes on shutdown if we index generators
    // by uri.spec but doesn't crash if we index by uri.spec.split().join().
    //this._localGenerators[generator.uri.spec] = generator;
    this._localGenerators[generator.uri.spec.split().join()] = generator;

    LOG("loaded local microsummary generator\n" +
        "  file: " + generator.localURI.spec + "\n" +
        "    ID: " + generator.uri.spec);
  },

  /**
   * Fix the ID of a local generator that was installed before we started
   * using URNs to uniquely identify local generators.
   *
   * @param   xmlDefinition
   *          an nsIDOMDocument XML document defining the generator
   * @param   localURI
   *          an nsIURI file URI to the generator's local file
   * 
   */
  _fixGeneratorID: function MSS__fixGeneratorID(xmlDefinition, localURI) {
    var generatorNode = xmlDefinition.getElementsByTagNameNS(MICSUM_NS, "generator")[0];

    if (!generatorNode)
      return;

    // Don't fix generators that have already been fixed or were installed
    // after we switched to identifying generators by URN.
    if (generatorNode.hasAttribute("uri"))
      return;

    // Don't fix generators that don't have any ID at all (we fall back to
    // the local URI in these cases, which is useful for developers during
    // the process of developing generators).
    if (!generatorNode.hasAttribute("sourceURI"))
      return;

    var oldURI = generatorNode.getAttribute("sourceURI");
    var newURI = "urn:source:" + oldURI;

    LOG("fixing generator with old-style ID\n" +
        "  old ID: " + oldURI + "\n" +
        "  new ID: " + newURI);

    // Update the XML definition to reflect the change.
    generatorNode.setAttribute("uri", newURI);

    // Save the updated XML definition to the local file.
    var file = localURI.QueryInterface(Ci.nsIFileURL).file.clone();
    this._saveGeneratorXML(xmlDefinition, file);

    // Update bookmarks in the bookmarks datastore that are using
    // this microsummary generator to reflect the change.
    this._changeField(FIELD_MICSUM_GEN_URI, oldURI, newURI);
  },

  // nsIMicrosummaryService

  /**
   * Install the microsummary generator from the resource at the supplied URI.
   * Callable by content via the addMicrosummaryGenerator() sidebar method.
   *
   * @param   generatorURI
   *          the URI of the resource providing the generator
   *
   */
  addGenerator: function MSS_addGenerator(generatorURI) {
    var t = this;
    var callback =
      function MSS_addGeneratorCallback(resource) {
        try     { t._handleNewGenerator(resource) }
        finally { resource.destroy() }
      };

    var resource = new MicrosummaryResource(generatorURI);
    resource.load(callback);
  },

  _handleNewGenerator: function MSS__handleNewGenerator(resource) {
    if (!resource.isXML)
      throw(resource.uri.spec + " microsummary generator loaded, but not XML");

    // XXX Make sure it's a valid microsummary generator.

    var rootNode = resource.content.documentElement;

    // Add a reference to the URI from which we got this generator so we have
    // a unique identifier for the generator and also so we can check back later
    // for updates.
    rootNode.setAttribute("uri", "urn:source:" + resource.uri.spec);

    this.installGenerator(resource.content);
  },
 
  /**
   * Install a microsummary generator from the given XML definition.
   *
   * @param   xmlDefinition
   *          an nsIDOMDocument XML document defining the generator
   *
   * @returns the newly-installed nsIMicrosummaryGenerator generator
   *
   */
  installGenerator: function MSS_installGenerator(xmlDefinition) {
    var rootNode = xmlDefinition.getElementsByTagNameNS(MICSUM_NS, "generator")[0];
 
    var generatorID = rootNode.getAttribute("uri");
 
    // The existing cache entry for this generator, if it is already installed.
    var generator = this._localGenerators[generatorID];

    var file;
    if (generator) {
      // This generator is already installed.  Save it in the existing file
      // (i.e. update the existing generator with the newly downloaded XML).
      file = generator.localURI.QueryInterface(Ci.nsIFileURL).file.clone();
    }
    else {
      // This generator is not already installed.  Save it as a new file.
      var generatorName = rootNode.getAttribute("name");
      var fileName = sanitizeName(generatorName) + ".xml";
      file = this._dirs.get("UsrMicsumGens", Ci.nsIFile);
      file.append(fileName);
      file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
      generator = new MicrosummaryGenerator();
      generator.localURI = this._ios.newFileURI(file);
      this._localGenerators[generatorID] = generator;
    }
 
    // Initialize (or reinitialize) the generator from its XML definition,
    // the save the definition to the generator's file.
    generator.initFromXML(xmlDefinition);
    this._saveGeneratorXML(xmlDefinition, file);

    LOG("installed generator " + generatorID);

    return generator;
  },

  /**
   * Save a generator's XML definition to a local file.
   *
   * @param   xmlDefinition
   *          an nsIDOMDocument XML document defining the generator
   * @param   file
   *          an nsIFile file representing the generator's local file
   * 
   */
  _saveGeneratorXML: function MSS_saveGeneratorXML(xmlDefinition, file) {
    LOG("saving definition to " + file.path);

    // Write the generator XML to the local file.
    var outputStream = Cc["@mozilla.org/network/safe-file-output-stream;1"].
                       createInstance(Ci.nsIFileOutputStream);
    var localFile = file.QueryInterface(Ci.nsILocalFile);
    outputStream.init(localFile, (MODE_WRONLY | MODE_TRUNCATE), PERMS_FILE, 0);
    var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
                     createInstance(Ci.nsIDOMSerializer);
    serializer.serializeToStream(xmlDefinition, outputStream, null);
    if (outputStream instanceof Ci.nsISafeOutputStream) {
      try       { outputStream.finish() }
      catch (e) { outputStream.close()  }
    }
    else
      outputStream.close();
  },

  /**
   * Get the set of microsummaries available for a given page.  The set
   * might change after this method returns, since this method will trigger
   * an asynchronous load of the page in question (if it isn't already loaded)
   * to see if it references any page-specific microsummaries.
   *
   * If the caller passes a bookmark ID, and one of the microsummaries
   * is the current one for the bookmark, this method will retrieve content
   * from the datastore for that microsummary, which is useful when callers
   * want to display a list of microsummaries for a page that isn't loaded,
   * and they want to display the actual content of the selected microsummary
   * immediately (rather than after the content is asynchronously loaded).
   *
   * @param   pageURI
   *          the URI of the page for which to retrieve available microsummaries
   *
   * @param   bookmarkID (optional)
   *          the ID of the bookmark for which this method is being called
   *
   * @returns an nsIMicrosummarySet of nsIMicrosummaries for the given page
   *
   */
  getMicrosummaries: function MSS_getMicrosummaries(pageURI, bookmarkID) {
    var microsummaries = new MicrosummarySet();

    // Get microsummaries defined by local generators.
    for (var genURISpec in this._localGenerators) {
      var generator = this._localGenerators[genURISpec];

      if (generator.appliesToURI(pageURI)) {
        var microsummary = new Microsummary(pageURI, generator);

        // If this is the current microsummary for this bookmark, load the content
        // from the datastore so it shows up immediately in microsummary picking UI.
        if (bookmarkID && this.isMicrosummary(bookmarkID, microsummary))
          microsummary.content = this._getField(bookmarkID, FIELD_GENERATED_TITLE);

        microsummaries.AppendElement(microsummary);
      }
    }

    // Get microsummaries defined by the page.  If we don't have the page,
    // download it asynchronously, and then finish populating the set.
    var resource = getLoadedMicrosummaryResource(pageURI);
    if (resource) {
      try     { microsummaries.extractFromPage(resource) }
      finally { resource.destroy() }
    }
    else {
      // Load the page with a callback that will add the page's microsummaries
      // to the set once the page has loaded.
      var callback = function MSS_extractFromPageCallback(resource) {
        try     { microsummaries.extractFromPage(resource) }
        finally { resource.destroy() }
      };

      try {
        resource = new MicrosummaryResource(pageURI);
        resource.load(callback);
      }
      catch(e) {
        // We don't have to do anything special if the call fails besides
        // destroying the Resource object.  We can just return the list
        // of microsummaries without including page-defined microsummaries.
        if (resource)
          resource.destroy();
        LOG("error downloading page to extract its microsummaries: " + e);
      }
    }

    return microsummaries;
  },

  /**
   * Change all occurrences of a specific value in a given field to a new value.
   *
   * @param   fieldName
   *          the name of the field whose values should be changed
   * @param   oldValue
   *          the value that should be changed
   * @param   newValue
   *          the value to which it should be changed
   *
   */
  _changeField: function MSS__changeField(fieldName, oldValue, newValue) {
    var bookmarks = this._getBookmarks();

    for ( var i = 0; i < bookmarks.length; i++ ) {
      var bookmarkID = bookmarks[i];

      if (this._hasField(bookmarkID, fieldName) &&
          this._getField(bookmarkID, fieldName) == oldValue)
        this._setField(bookmarkID, fieldName, newValue);
    }
  },

//@line 705 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
  /**
   * Get the set of bookmarks with microsummaries.
   *
   * This is the internal version of this method, which is not accessible
   * via XPCOM but is more performant; inside this component, use this version.
   * Outside the component, use getBookmarks (no underscore prefix) instead.
   *
   * @returns an array of bookmark IDs
   *
   */
  _getBookmarks: function MSS__getBookmarks() {
    var bookmarks = [];

    var resources = this._bmds.GetSources(this._resource(FIELD_RDF_TYPE),
                                          this._resource(VALUE_MICSUM_BOOKMARK),
                                          true);
    while (resources.hasMoreElements()) {
      var resource = resources.getNext().QueryInterface(Ci.nsIRDFResource);

      // When a bookmark gets deleted or cut, most of its arcs get removed
      // from the data source, but a few of them remain, in particular its RDF
      // type arc.  So just because this resource has a MicsumBookmark type,
      // that doesn't mean it's a real bookmark!  We need to check.
      if (!this._bms.isBookmarkedResource(resource))
        continue;

      bookmarks.push(resource);
    }

    return bookmarks;
  },

  _getField: function MSS__getField(bookmarkID, fieldName) {
    var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
    var fieldValue;

    var node = this._bmds.GetTarget(bookmarkResource,
                                    this._resource(fieldName),
                                    true);
    if (node) {
      if (fieldName == FIELD_RDF_TYPE)
        fieldValue = node.QueryInterface(Ci.nsIRDFResource).Value;
      else
        fieldValue = node.QueryInterface(Ci.nsIRDFLiteral).Value;
    }
    else
      fieldValue = null;

    return fieldValue;
  },

  _setField: function MSS__setField(bookmarkID, fieldName, fieldValue) {
    var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);

    if (this._hasField(bookmarkID, fieldName)) {
      var oldValue = this._getField(bookmarkID, fieldName);
      this._bmds.Change(bookmarkResource,
                        this._resource(fieldName),
                        this._literal(oldValue),
                        this._literal(fieldValue));
    }
    else {
      this._bmds.Assert(bookmarkResource,
                        this._resource(fieldName),
                        this._literal(fieldValue),
                        true);
    }
  },

  _clearField: function MSS__clearField(bookmarkID, fieldName) {
    var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);

    var node = this._bmds.GetTarget(bookmarkResource,
                                    this._resource(fieldName),
                                    true);
    if (node) {
      this._bmds.Unassert(bookmarkResource,
                          this._resource(fieldName),
                          node);
    }
  },

  _hasField: function MSS__hasField(bookmarkID, fieldName) {
    var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);

    var node = this._bmds.GetTarget(bookmarkResource,
                                    this._resource(fieldName),
                                    true);
    return node ? true : false;
  },

  /**
   * Get the URI of the page to which a given bookmark refers.
   *
   * @param   bookmarkResource
   *          an nsIResource uniquely identifying the bookmark
   *
   * @returns an nsIURI object representing the bookmark's page,
   *          or null if the bookmark doesn't exist
   *
   */
  _getPageForBookmark: function MSS__getPageForBookmark(bookmarkID) {
    var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);

    var node = this._bmds.GetTarget(bookmarkResource,
                                    this._resource(NC_NS + "URL"),
                                    true);

    if (!node)
      return null;

    var pageSpec = node.QueryInterface(Ci.nsIRDFLiteral).Value;
    var pageURI = this._uri(pageSpec);
    return pageURI;
  },
//@line 821 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

  /**
   * Get the set of bookmarks with microsummaries.
   *
   * Bookmark IDs are nsIRDFResource objects on builds with old RDF-based
   * bookmarks and nsIURI objects on builds with new Places-based bookmarks.
   *
   * This is the external version of this method and is accessible via XPCOM.
   * Use it outside this component. Inside the component, use _getBookmarks
   * (with underscore prefix) instead for performance.
   *
   * @returns an nsISimpleEnumerator enumeration of bookmark IDs
   *
   */
  getBookmarks: function MSS_getBookmarks() {
    return new ArrayEnumerator(this._getBookmarks());
  },

  /**
   * Get the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to get the current microsummary
   *
   * @returns the current microsummary for the bookmark, or null
   *          if the bookmark does not have a current microsummary
   *
   */
  getMicrosummary: function MSS_getMicrosummary(bookmarkID) {
    if (!this.hasMicrosummary(bookmarkID))
      return null;

    var pageURI = this._getPageForBookmark(bookmarkID);
    var generatorURI = this._uri(this._getField(bookmarkID, FIELD_MICSUM_GEN_URI));
    
    var localGenerator = this._localGenerators[generatorURI.spec];

    var microsummary = new Microsummary(pageURI, localGenerator);
    if (!localGenerator)
      microsummary.generator.uri = generatorURI;

    return microsummary;
  },

  /**
   * Set the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to set the current microsummary
   *
   * @param   microsummary
   *          the microsummary to set as the current one
   *
   */
  setMicrosummary: function MSS_setMicrosummary(bookmarkID, microsummary) {
//@line 877 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
    // Make sure that the bookmark is of type MicsumBookmark
    // because that's what the template rules are matching
    if (this._getField(bookmarkID, FIELD_RDF_TYPE) != VALUE_MICSUM_BOOKMARK) {
      // Force the bookmark trees to rebuild, since they don't seem
      // to be rebuilding on their own (bug 348928).
      this._bmds.beginUpdateBatch();

      var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
      if (this._hasField(bookmarkID, FIELD_RDF_TYPE)) {
        var oldValue = this._getField(bookmarkID, FIELD_RDF_TYPE);
        this._bmds.Change(bookmarkResource,
                          this._resource(FIELD_RDF_TYPE),
                          this._resource(oldValue),
                          this._resource(VALUE_MICSUM_BOOKMARK));
      }
      else {
        this._bmds.Assert(bookmarkResource,
                          this._resource(FIELD_RDF_TYPE),
                          this._resource(VALUE_MICSUM_BOOKMARK),
                          true);
      }

      this._bmds.endUpdateBatch();
    }
//@line 902 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
    this._setField(bookmarkID, FIELD_MICSUM_GEN_URI, microsummary.generator.uri.spec);

    if (microsummary.content) {
      this._updateMicrosummary(bookmarkID, microsummary);
    }
    else {
      // Display a static title initially (unless there's already one set)
      if (!this._getField(bookmarkID, FIELD_GENERATED_TITLE))
        this._setField(bookmarkID, FIELD_GENERATED_TITLE,
                       microsummary.generator.name || microsummary.generator.uri.spec);

      this.refreshMicrosummary(bookmarkID);
    }
  },

  /**
   * Remove the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to remove the current microsummary
   *
   */
  removeMicrosummary: function MSS_removeMicrosummary(bookmarkID) {
//@line 926 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
    // Set the bookmark's RDF type back to the normal bookmark type
    if (this._getField(bookmarkID, FIELD_RDF_TYPE) == VALUE_MICSUM_BOOKMARK) {
      // Force the bookmark trees to rebuild, since they don't seem
      // to be rebuilding on their own (bug 348928).
      this._bmds.beginUpdateBatch();

      var bookmarkResource = bookmarkID.QueryInterface(Ci.nsIRDFResource);
      this._bmds.Change(bookmarkResource,
                        this._resource(FIELD_RDF_TYPE),
                        this._resource(VALUE_MICSUM_BOOKMARK),
                        this._resource(VALUE_NORMAL_BOOKMARK));

      this._bmds.endUpdateBatch();
    }
//@line 941 "D:/builds/nightlies/firefox/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

    var fields = [FIELD_MICSUM_GEN_URI,
                  FIELD_MICSUM_EXPIRATION,
                  FIELD_GENERATED_TITLE,
                  FIELD_CONTENT_TYPE];

    for ( var i = 0; i < fields.length; i++ ) {
      var field = fields[i];
      if (this._hasField(bookmarkID, field))
        this._clearField(bookmarkID, field);
    }
  },

  /**
   * Whether or not the given bookmark has a current microsummary.
   *
   * @param   bookmarkID
   *          the bookmark for which to set the current microsummary
   *
   * @returns a boolean representing whether or not the given bookmark
   *          has a current microsummary
   *
   */
  hasMicrosummary: function MSS_hasMicrosummary(bookmarkID) {
    return this._hasField(bookmarkID, FIELD_MICSUM_GEN_URI);
  },

  /**
   * Whether or not the given microsummary is the current microsummary
   * for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark to check
   *
   * @param   microsummary
   *          the microsummary to check
   *
   * @returns whether or not the microsummary is the current one
   *          for the bookmark
   *
   */
  isMicrosummary: function MSS_isMicrosummary(bookmarkID, microsummary) {
    if (!this.hasMicrosummary(bookmarkID))
      return false;

    var currentGen = this._getField(bookmarkID, FIELD_MICSUM_GEN_URI);

    if (microsummary.generator.uri.equals(this._uri(currentGen)))
      return true;

    return false
  },

  /**
   * Refresh a microsummary, updating its value in the datastore and UI.
   * If this method can refresh the microsummary instantly, it will.
   * Otherwise, it'll asynchronously download the necessary information
   * (the generator and/or page) before refreshing the microsummary.
   *
   * Callers should check the "content" property of the returned microsummary
   * object to distinguish between sync and async refreshes.  If its value
   * is "null", then it's an async refresh, and the caller should register
   * itself as an nsIMicrosummaryObserver via nsIMicrosummary.addObserver()
   * to find out when the refresh completes.
   *
   * @param   bookmarkID
   *          the bookmark whose microsummary is being refreshed
   *
   * @returns the microsummary being refreshed
   *
   */
  refreshMicrosummary: function MSS_refreshMicrosummary(bookmarkID) {
    if (!this.hasMicrosummary(bookmarkID))
      throw "bookmark " + bookmarkID + " does not have a microsummary";

    var pageURI = this._getPageForBookmark(bookmarkID);
    if (!pageURI)
      throw("can't get URL for bookmark with ID " + bookmarkID);
    var generatorURI = this._uri(this._getField(bookmarkID, FIELD_MICSUM_GEN_URI));

    var localGenerator = this._localGenerators[generatorURI.spec];

    var microsummary = new Microsummary(pageURI, localGenerator);
    if (!localGenerator)
      microsummary.generator.uri = generatorURI;

    // A microsummary observer that calls the microsummary service
    // to update the datastore when the microsummary finishes loading.
    var observer = {
      _svc: this,
      _bookmarkID: bookmarkID,
      onContentLoaded: function MSS_observer_onContentLoaded(microsummary) {
        try {
          this._svc._updateMicrosummary(this._bookmarkID, microsummary);
        }
        finally {
          this._svc = null;
          this._bookmarkID = null;
          microsummary.removeObserver(this);
        }
      }
    };

    // Register the observer with the microsummary and trigger the microsummary
    // to update itself.
    microsummary.addObserver(observer);
    microsummary.update();
    
    return microsummary;
  }
};





function Microsummary(pageURI, generator) {
  this._observers = [];
  this.pageURI = pageURI;
  this.generator = generator ? generator : new MicrosummaryGenerator();
}

Microsummary.prototype = {
  // The microsummary service.
  __mss: null,
  get _mss() {
    if (!this.__mss)
      this.__mss = Cc["@mozilla.org/microsummary/service;1"].
                   getService(Ci.nsIMicrosummaryService);
    return this.__mss;
  },

  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  /**
   * Make a URI from a spec.
   * @param   spec
   *          The string spec of the URI.
   * @returns An nsIURI object.
   */
  _uri: function MSS__uri(spec) {
    return this._ios.newURI(spec, null, null);
  },

  interfaces: [Ci.nsIMicrosummary, Ci.nsISupports],

  // nsISupports

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummary) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  // nsIMicrosummary

  _content: null,
  get content() {
    // If we have everything we need to generate the content, generate it.
    if (this._content == null &&
        this.generator.loaded &&
        (this.pageContent || !this.generator.needsPageContent)) {
      this._content = this.generator.generateMicrosummary(this.pageContent);
      this.updateInterval = this.generator.calculateUpdateInterval(this.pageContent);
    }

    // Note: we return "null" if the content wasn't already generated and we
    // couldn't retrieve it from the generated title annotation or generate it
    // ourselves.  So callers shouldn't count on getting content; instead,
    // they should call update if the return value of this getter is "null",
    // setting an observer to tell them when content generation is done.
    return this._content;
  },
  set content(newValue) { this._content = newValue },

  _generator: null,
  get generator()            { return this._generator },
  set generator(newValue)    { this._generator = newValue },

  _pageURI: null,
  get pageURI()              { return this._pageURI },
  set pageURI(newValue)      { this._pageURI = newValue },

  _pageContent: null,
  get pageContent() {
    if (!this._pageContent) {
      // If the page is currently loaded into a browser window, use that.
      var resource = getLoadedMicrosummaryResource(this.pageURI);
      if (resource) {
        this._pageContent = resource.content;
        resource.destroy();
      }
    }

    return this._pageContent;
  },
  set pageContent(newValue) { this._pageContent = newValue },

  _updateInterval: null,
  get updateInterval()         { return this._updateInterval; },
  set updateInterval(newValue) { this._updateInterval = newValue; },

  // nsIMicrosummary

  _observers: null,

  addObserver: function MS_addObserver(observer) {
    // Register the observer, but only if it isn't already registered,
    // so that we don't call the same observer twice for any given change.
    if (this._observers.indexOf(observer) == -1)
      this._observers.push(observer);
  },
  
  removeObserver: function MS_removeObserver(observer) {
    //NS_ASSERT(this._observers.indexOf(observer) != -1,
    //          "can't remove microsummary observer " + observer + ": not registered");
  
    //this._observers =
    //  this._observers.filter(function(i) { observer != i });
    if (this._observers.indexOf(observer) != -1)
      this._observers.splice(this._observers.indexOf(observer), 1);
  },

  /**
   * Regenerates the microsummary, asynchronously downloading its generator
   * and content as needed.
   *
   */
  update: function MS_update() {
    LOG("microsummary.update called for page:\n  " + this.pageURI.spec +
        "\nwith generator:\n  " + this.generator.uri.spec);

    var t = this;

    // If we don't have the generator, download it now.  After it downloads,
    // we'll re-call this method to continue updating the microsummary.
    if (!this.generator.loaded) {
      // If this generator is identified by a URN, then it's a local generator
      // that should have been cached on application start, so it's missing.
      if (this.generator.uri.scheme == "urn") {
        // If it was installed via nsSidebar::addMicrosummaryGenerator (i.e. it
        // has a URN that identifies the source URL from which we installed it),
        // try to reinstall it (once).
        if (/^source:/.test(this.generator.uri.path)) {
          this._reinstallMissingGenerator();
          return;
        }
        else
          throw "missing local generator: " + this.generator.uri.spec;
      }

      LOG("generator not yet loaded; downloading it");
      var generatorCallback =
        function MS_generatorCallback(resource) {
          try     { t._handleGeneratorLoad(resource) }
          finally { resource.destroy() }
        };
      var resource = new MicrosummaryResource(this.generator.uri);
      resource.load(generatorCallback);
      return;
    }

    // If we need the page content, and we don't have it, download it now.
    // Afterwards we'll re-call this method to continue updating the microsummary.
    if (this.generator.needsPageContent && !this.pageContent) {
      LOG("page content not yet loaded; downloading it");
      var pageCallback =
        function MS_pageCallback(resource) {
          try     { t._handlePageLoad(resource) }
          finally { resource.destroy() }
        };
      var resource = new MicrosummaryResource(this.pageURI);
      resource.load(pageCallback);
      return;
    }

    LOG("generator (and page, if needed) both loaded; generating microsummary");

    // Now that we have both the generator and (if needed) the page content,
    // generate the microsummary, then let the observers know about it.
    this.content = this.generator.generateMicrosummary(this.pageContent);
    this.updateInterval = this.generator.calculateUpdateInterval(this.pageContent);
    this.pageContent = null;
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onContentLoaded(this);

    LOG("generated microsummary: " + this.content);
  },

  _handleGeneratorLoad: function MS__handleGeneratorLoad(resource) {
    LOG(this.generator.uri.spec + " microsummary generator downloaded");
    if (resource.isXML)
      this.generator.initFromXML(resource.content);
    else if (resource.contentType == "text/plain")
      this.generator.initFromText(resource.content);
    else if (resource.contentType == "text/html")
      this.generator.initFromText(resource.content.body.textContent);
    else
      throw("generator is neither XML nor plain text");

    // Only trigger a [content] update if we were able to init the generator. 
    if (this.generator.loaded)
      this.update();
  },

  _handlePageLoad: function MS__handlePageLoad(resource) {
    if (!resource.isXML && resource.contentType != "text/html")
      throw("page is neither HTML nor XML");

    this.pageContent = resource.content;
    this.update();
  },

  /**
   * Try to reinstall a missing local generator that was originally installed
   * from a URL using nsSidebar::addMicrosumaryGenerator.
   *
   */
  _reinstallMissingGenerator: function MS__reinstallMissingGenerator() {
    LOG("attempting to reinstall missing generator " + this.generator.uri.spec);

    var t = this;

    var loadCallback =
      function MS_missingGeneratorLoadCallback(resource) {
        try     { t._handleMissingGeneratorLoad(resource) }
        finally { resource.destroy() }
      };

    var errorCallback =
      function MS_missingGeneratorErrorCallback(resource) {
        try     { t._handleMissingGeneratorError(resource) }
        finally { resource.destroy() }
      };

    try {
      // Extract the URI from which the generator was originally installed.
      var sourceURL = this.generator.uri.path.replace(/^source:/, "");
      var sourceURI = this._uri(sourceURL);

      var resource = new MicrosummaryResource(sourceURI);
      resource.load(loadCallback, errorCallback);
    }
    catch(ex) {
      Components.utils.reportError(ex);
      this._handleMissingGeneratorError();
    }
  },

  /**
   * Handle a load event for a missing local generator by trying to reinstall
   * the generator.  If this fails, call _handleMissingGeneratorError to unset
   * microsummaries for bookmarks using this generator so we don't repeatedly
   * try to reinstall the generator, creating too much traffic to the website
   * from which we downloaded it.
   *
   * @param resource
   *        the nsIMicrosummaryResource representing the downloaded generator
   *
   */
  _handleMissingGeneratorLoad: function MS__handleMissingGeneratorLoad(resource) {
    try {
      // Make sure the generator is XML, since local generators have to be.
      if (!resource.isXML)
        throw("downloaded, but not XML " + this.generator.uri.spec);

      // Store the generator's ID in its XML definition.
      var generatorID = this.generator.uri.spec;
      resource.content.documentElement.setAttribute("uri", generatorID);

      // Reinstall the generator and replace our placeholder generator object
      // with the newly installed generator.
      this.generator = this._mss.installGenerator(resource.content);

      // A reinstalled generator should always be loaded.  But just in case
      // it isn't, throw an error so we don't get into an infinite loop
      // (otherwise this._update would try again to reinstall it).
      if (!this.generator.loaded)
        throw("supposedly installed, but not in cache " + this.generator.uri.spec);
    }
    catch(ex) {
      Components.utils.reportError(ex);
      this._handleMissingGeneratorError(resource);
      return;
    }
  
    LOG("reinstall succeeded; resuming update " + this.generator.uri.spec);
    this.update();
  },

  /**
   * Handle an error event for a missing local generator load by unsetting
   * the microsummaries for bookmarks using this generator so we don't
   * repeatedly try to reinstall the generator, creating too much traffic
   * to the website from which we downloaded it.
   *
   * @param resource
   *        the nsIMicrosummaryResource representing the downloaded generator
   *
   */
  _handleMissingGeneratorError: function MS__handleMissingGeneratorError(resource) {
    LOG("reinstall failed; removing microsummaries " + this.generator.uri.spec);
    var bookmarks = this._mss.getBookmarks();
    while (bookmarks.hasMoreElements()) {
      var bookmarkID = bookmarks.getNext();
      var microsummary = this._mss.getMicrosummary(bookmarkID);
      if (microsummary.generator.uri.equals(this.generator.uri)) {
        LOG("removing microsummary for " + microsummary.pageURI.spec);
        this._mss.removeMicrosummary(bookmarkID);
      }
    }
  }

};





function MicrosummaryGenerator() {}

MicrosummaryGenerator.prototype = {

  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  interfaces: [Ci.nsIMicrosummaryGenerator, Ci.nsISupports],

  // nsISupports

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummaryGenerator) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  // nsIMicrosummaryGenerator

  // Normally this is just the URL from which we download the generator,
  // but for generators stored in the app or profile generators directory
  // it's the value of the generator tag's "uri" attribute (or its local URI
  // should the "uri" attribute be missing).
  _uri: null,
  get uri() { return this._uri || this.localURI },
  set uri(newValue) { this._uri = newValue },

  // For generators bundled with the browser or installed by the user,
  // the local URI is the URI of the local file containing the generator XML.
  _localURI: null,
  get localURI() { return this._localURI },
  set localURI(newValue) { this._localURI = newValue },

  _name: null,
  get name() { return this._name },
  set name(newValue) { this._name = newValue },

  _template: null,
  get template() { return this._template },
  set template(newValue) { this._template = newValue },

  _content: null,
  get content() { return this._content },
  set content(newValue) { this._content = newValue },

  _loaded: false,
  get loaded() { return this._loaded },
  set loaded(newValue) { this._loaded = newValue },

  _rules: null,

  /**
   * Determines whether or not the generator applies to a given URI.
   * By default, the generator does not apply to any URI.  In order for it
   * to apply to a URI, the URI must match one or more of the generator's
   * "include" rules and not match any of the generator's "exclude" rules.
   *
   * @param   uri
   *          the URI to test to see if this generator applies to it
   *
   * @returns boolean
   *          whether or not the generator applies to the given URI
   *
   */
  appliesToURI: function(uri) {
    var applies = false;

    for ( var i = 0 ; i < this._rules.length ; i++ ) {
      var rule = this._rules[i];

      switch (rule.type) {
      case "include":
        if (rule.regexp.test(uri.spec))
          applies = true;
        break;
      case "exclude":
        if (rule.regexp.test(uri.spec))
          return false;
        break;
      }
    }

    return applies;
  },

  get needsPageContent() {
    if (this.template)
      return true;
    else if (this.content)
      return false;
    else
      throw("needsPageContent called on uninitialized microsummary generator");
  },

  /**
   * Initializes a generator from text content.  Generators initialized
   * from text content merely return that content when their generate() method
   * gets called.
   *
   * @param   text
   *          the text content
   *
   */
  initFromText: function(text) {
    this.content = text;
    this.loaded = true;
  },

  /**
   * Initializes a generator from an XML description of it.
   * 
   * @param   xmlDocument
   *          An XMLDocument object describing a microsummary generator.
   *
   */
  initFromXML: function(xmlDocument) {
    // XXX Make sure the argument is a valid generator XML document.

    // XXX I would have wanted to retrieve the info from the XML via E4X,
    // but we'll need to pass the XSLT transform sheet to the XSLT processor,
    // and the processor can't deal with an E4X-wrapped template node.

    // XXX Right now the code retrieves the first "generator" element
    // in the microsummaries namespace, regardless of whether or not
    // it's the root element.  Should it matter?
    var generatorNode = xmlDocument.getElementsByTagNameNS(MICSUM_NS, "generator")[0];
    if (!generatorNode)
      throw Components.results.NS_ERROR_FAILURE;

    this.name = generatorNode.getAttribute("name");

    // If this is a local generator (i.e. it has a local URI), then we have
    // to retrieve its URI from the "uri" attribute of its generator tag.
    if (this.localURI && generatorNode.hasAttribute("uri")) {
      this.uri = this._ios.newURI(generatorNode.getAttribute("uri"), null, null);
    }

    function getFirstChildByTagName(tagName, parentNode, namespace) {
      var nodeList = parentNode.getElementsByTagNameNS(namespace, tagName);
      for (var i = 0; i < nodeList.length; i++) {
        // Make sure that the node is a direct descendent of the generator node
        if (nodeList[i].parentNode == parentNode)
          return nodeList[i];
      }
      return null;
    }

    // Slurp the include/exclude rules that determine the pages to which
    // this generator applies.  Order is important, so we add the rules
    // in the order in which they appear in the XML.
    this._rules = [];
    var pages = getFirstChildByTagName("pages", generatorNode, MICSUM_NS);
    if (pages) {
      // XXX Make sure the pages tag exists.
      for ( var i = 0; i < pages.childNodes.length ; i++ ) {
        var node = pages.childNodes[i];
        if (node.nodeType != node.ELEMENT_NODE ||
            node.namespaceURI != MICSUM_NS ||
            (node.nodeName != "include" && node.nodeName != "exclude"))
          continue;
        var urlRegexp = node.textContent.replace(/^\s+|\s+$/g, "");
        this._rules.push({ type: node.nodeName, regexp: new RegExp(urlRegexp) });
      }
    }

    // allow the generators to set individual update values (even varying
    // depending on certain XPath expressions)
    var update = getFirstChildByTagName("update", generatorNode, MICSUM_NS);
    if (update) {
      function _parseInterval(string) {
        // convert from minute fractions to milliseconds
        // and ensure a minimum value of 1 minute
        return Math.round(Math.max(parseFloat(string) || 0, 1) * 60 * 1000);
      }

      this._unconditionalUpdateInterval =
        update.hasAttribute("interval") ?
        _parseInterval(update.getAttribute("interval")) : null;

      // collect the <condition expression="XPath Expression" interval="time"/> clauses
      this._updateIntervals = new Array();
      for (i = 0; i < update.childNodes.length; i++) {
        node = update.childNodes[i];
        if (node.nodeType != node.ELEMENT_NODE || node.namespaceURI != MICSUM_NS ||
            node.nodeName != "condition")
          continue;
        if (!node.getAttribute("expression") || !node.getAttribute("interval")) {
          LOG("ignoring incomplete conditional update interval for " + this.uri.spec);
          continue;
        }
        this._updateIntervals.push({
          expression: node.getAttribute("expression"),
          interval: _parseInterval(node.getAttribute("interval"))
        });
      }
    }

    var templateNode = getFirstChildByTagName("template", generatorNode, MICSUM_NS);
    if (templateNode) {
      this.template = getFirstChildByTagName("transform", templateNode, XSLT_NS) ||
                      getFirstChildByTagName("stylesheet", templateNode, XSLT_NS);
    }
    // XXX Make sure the template is a valid XSL transform sheet.

    this.loaded = true;
  },

  generateMicrosummary: function MSD_generateMicrosummary(pageContent) {

    var content;

    if (this.content) {
      content = this.content;
    } else if (this.template) {
      content = this._processTemplate(pageContent);
    } else
      throw("generateMicrosummary called on uninitialized microsummary generator");

    // Clean up the output
    content = content.replace(/^\s+|\s+$/g, "");
    if (content.length > MAX_SUMMARY_LENGTH) 
      content = content.substring(0, MAX_SUMMARY_LENGTH);

    return content;
  },

  calculateUpdateInterval: function MSD_calculateUpdateInterval(doc) {
    if (this.content || !this._updateIntervals || !doc)
      return null;

    for (var i = 0; i < this._updateIntervals.length; i++) {
      try {
        if (doc.evaluate(this._updateIntervals[i].expression, doc, null,
                         Ci.nsIDOMXPathResult.BOOLEAN_TYPE, null).booleanValue)
          return this._updateIntervals[i].interval;
      }
      catch (ex) {
        Components.utils.reportError(ex);
        // remove the offending conditional update interval
        this._updateIntervals.splice(i--, 1);
      }
    }

    return this._unconditionalUpdateInterval;
  },

  _processTemplate: function MSD__processTemplate(doc) {
    LOG("processing template " + this.template + " against document " + doc);

    // XXX Should we just have one global instance of the processor?
    var processor = Cc["@mozilla.org/document-transformer;1?type=xslt"].
                    createInstance(Ci.nsIXSLTProcessor);

    // Turn off document loading of all kinds (document(), <include>, <import>)
    // for security (otherwise local generators would be able to load local files).
    processor.flags |= Ci.nsIXSLTProcessorPrivate.DISABLE_ALL_LOADS;

    processor.importStylesheet(this.template);
    var fragment = processor.transformToFragment(doc, doc);

    LOG("template processing result: " + fragment.textContent);

    // XXX When we support HTML microsummaries we'll need to do something
    // more sophisticated than just returning the text content of the fragment.
    return fragment.textContent;
  }
};





// Microsummary sets are collections of microsummaries.  They allow callers
// to register themselves as observers of the set, and when any microsummary
// in the set changes, the observers get notified.  Thus a caller can observe
// the set instead of each individual microsummary.

function MicrosummarySet() {
  this._observers = [];
  this._elements = [];
}

MicrosummarySet.prototype = {

  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  interfaces: [Ci.nsIMicrosummarySet,
               Ci.nsIMicrosummaryObserver,
               Ci.nsISupports],

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummarySet) &&
        !iid.equals(Ci.nsIMicrosummaryObserver) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  _observers: null,
  _elements: null,

  // nsIMicrosummaryObserver

  onContentLoaded: function MSSet_onContentLoaded(microsummary) {
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onContentLoaded(microsummary);
  },

  // nsIMicrosummarySet

  addObserver: function MSSet_addObserver(observer) {
    if (this._observers.length == 0) {
      for ( var i = 0 ; i < this._elements.length ; i++ )
        this._elements[i].addObserver(this);
    }

    // Register the observer, but only if it isn't already registered,
    // so that we don't call the same observer twice for any given change.
    if (this._observers.indexOf(observer) == -1)
      this._observers.push(observer);
  },
  
  removeObserver: function MSSet_removeObserver(observer) {
    //NS_ASSERT(this._observers.indexOf(observer) != -1,
    //          "can't remove microsummary observer " + observer + ": not registered");
  
    //this._observers =
    //  this._observers.filter(function(i) { observer != i });
    if (this._observers.indexOf(observer) != -1)
      this._observers.splice(this._observers.indexOf(observer), 1);
    
    if (this._observers.length == 0) {
      for ( var i = 0 ; i < this._elements.length ; i++ )
        this._elements[i].removeObserver(this);
    }
  },

  extractFromPage: function MSSet_extractFromPage(resource) {
    if (!resource.isXML && resource.contentType != "text/html")
      throw("page is neither HTML nor XML");

    // XXX Handle XML documents, whose microsummaries are specified
    // via processing instructions.

    var links = resource.content.getElementsByTagName("link");
    for ( var i = 0; i < links.length; i++ ) {
      var link = links[i];

      if(!link.hasAttribute("rel"))
        continue;

      var relAttr = link.getAttribute("rel");

      // The attribute's value can be a space-separated list of link types,
      // check to see if "microsummary" is one of them.
      var linkTypes = relAttr.split(/\s+/);
      if (!linkTypes.some( function(v) { return v.toLowerCase() == "microsummary"; }))
        continue;


      // Look for a TITLE attribute to give the generator a nice name in the UI.
      var linkTitle = link.getAttribute("title");


      // Unlike the "href" attribute, the "href" property contains
      // an absolute URI spec, so we use it here to create the URI.
      var generatorURI = this._ios.newURI(link.href,
                                          resource.content.characterSet,
                                          null);

      try {
        const securityManager = Cc["@mozilla.org/scriptsecuritymanager;1"].
                                getService(Ci.nsIScriptSecurityManager);
        securityManager.checkLoadURI(resource.uri,
                                     generatorURI,
                                     Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
      }
      catch(e) {
        LOG("can't load generator " + generatorURI.spec + " from page " +
            resource.uri.spec + ": " + e);
        continue;
      }

      var microsummary = new Microsummary(resource.uri, null);
      microsummary.generator.name = linkTitle;
      microsummary.generator.uri  = generatorURI;
      this.AppendElement(microsummary);
    }
  },

  // XXX Turn this into a complete implementation of nsICollection?
  AppendElement: function MSSet_AppendElement(element) {
    // Query the element to a microsummary.
    // XXX Should we NS_ASSERT if this fails?
    element = element.QueryInterface(Ci.nsIMicrosummary);

    if (this._elements.indexOf(element) == -1) {
      this._elements.push(element);
      element.addObserver(this);
    }

    // Notify observers that an element has been appended.
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onElementAppended(element);
  },

  Enumerate: function MSSet_Enumerate() {
    return new ArrayEnumerator(this._elements);
  }
};





/**
 * An enumeration of items in a JS array.
 * @constructor
 */
function ArrayEnumerator(aItems) {
  this._index = 0;
  if (aItems) {
    for (var i = 0; i < aItems.length; ++i) {
      if (!aItems[i])
        aItems.splice(i, 1);      
    }
  }
  this._contents = aItems;
}

ArrayEnumerator.prototype = {
  interfaces: [Ci.nsISimpleEnumerator, Ci.nsISupports],

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsISimpleEnumerator) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  _index: 0,
  _contents: [],
  
  hasMoreElements: function() {
    return this._index < this._contents.length;
  },
  
  getNext: function() {
    return this._contents[this._index++];      
  }
};





/**
 * Outputs aText to the JavaScript console as well as to stdout if the
 * microsummary logging pref (browser.microsummary.log) is set to true.
 * 
 * @param aText
 *        the text to log
 */
function LOG(aText) {
  if (getPref("browser.microsummary.log", false)) {
    dump("*** Microsummaries: " +  aText + "\n");
    var consoleService = Cc["@mozilla.org/consoleservice;1"].
                         getService(Ci.nsIConsoleService);
    consoleService.logStringMessage(aText);
  }
}





/**
 * A resource (page, microsummary, generator, etc.) identifiable by URI.
 * This object abstracts away much of the code for loading resources
 * and parsing their content if they are XML or HTML.
 * 
 * @constructor
 * 
 * @param   uri
 *          the location of the resource
 *
 */
function MicrosummaryResource(uri) {
  // Make sure we're not loading javascript: or data: URLs, which could
  // take advantage of the load to run code with chrome: privileges.
  // XXX Perhaps use nsIScriptSecurityManager.checkLoadURI instead.
  if (uri.scheme != "http" && uri.scheme != "https" && uri.scheme != "file")
    throw NS_ERROR_DOM_BAD_URI;

  this._uri = uri;
}

MicrosummaryResource.prototype = {
  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  _uri: null,
  get uri() {
    return this._uri;
  },

  _content: null,
  get content() {
    return this._content;
  },

  _contentType: null,
  get contentType() {
    return this._contentType;
  },

  _isXML: false,
  get isXML() {
    return this._isXML;
  },

  // A function to call when we finish loading/parsing the resource.
  _loadCallback: null,

  // A function to call if we get an error while loading/parsing the resource.
  _errorCallback: null,

  // A hidden iframe to parse HTML content.
  _iframe: null,

  // Implement notification callback interfaces so we can suppress UI
  // and abort loads for bad SSL certs and HTTP authorization requests.
  
  // Interfaces this component implements.
  interfaces: [Ci.nsIBadCertListener,
               Ci.nsIAuthPromptProvider,
               Ci.nsIAuthPrompt,
               Ci.nsIProgressEventSink,
               Ci.nsIInterfaceRequestor,
               Ci.nsISupports],

  // nsISupports

  QueryInterface: function MSR_QueryInterface(iid) {
    if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
      throw Components.results.NS_ERROR_NO_INTERFACE;

    return this;
  },

  // nsIInterfaceRequestor
  
  getInterface: function MSR_getInterface(iid) {
    return this.QueryInterface(iid);
  },

  // nsIBadCertListener

  // Suppress UI and abort secure loads from servers with bad SSL certificates.
  
  confirmUnknownIssuer: function MSR_confirmUnknownIssuer(socketInfo, cert, certAddType) {
    return false;
  },

  confirmMismatchDomain: function MSR_confirmMismatchDomain(socketInfo, targetURL, cert) {
    return false;
  },

  confirmCertExpired: function MSR_confirmCertExpired(socketInfo, cert) {
    return false;
  },

  notifyCrlNextupdate: function MSR_notifyCrlNextupdate(socketInfo, targetURL, cert) {
  },

  // nsIAuthPromptProvider
  
  // Suppress UI and abort loads for files secured by HTTP authentication.

  // HTTP auth requests appear to succeed when we cancel them (since the server
  // redirects us to a "you're not authorized" page), so we have to set a flag
  // to let the load handler know to treat the load as a failure.
  __httpAuthFailed: false,
  get _httpAuthFailed()         { return this.__httpAuthFailed },
  set _httpAuthFailed(newValue) { this.__httpAuthFailed = newValue },

  getAuthPrompt: function(aPromptReason, aIID) {
    this._httpAuthFailed = true;
    throw Components.results.NS_ERROR_NOT_AVAILABLE;
  },

  // nsIAuthPrompt

  // XXX If necko always requests nsIAuthPromptProvider before requesting
  // nsIAuthPrompt, then we probably only have to implement the provider.

  prompt: function(dialogTitle, text, passwordRealm, savePassword, defaultText, result) {
    this._httpAuthFailed = true;
    return false;
  },

  promptUsernameAndPassword: function(dialogTitle, text, passwordRealm, savePassword, user, pwd) {
    this._httpAuthFailed = true;
    return false;
  },

  promptPassword: function(dialogTitle, text, passwordRealm, savePassword, pwd) {
    this._httpAuthFailed = true;
    return false;
  },

  // XXX We implement nsIProgressEventSink because otherwise bug 253127
  // would cause too many extraneous errors to get reported to the console.
  // Fortunately this doesn't screw up XMLHttpRequest, because it ensures
  // that its implementation of nsIProgressEventSink will always get called
  // in addition to whatever notification callbacks we set on the channel
  // (this is not true for most other interfaces, so we should be conservative
  // about what we implement/override, even in the face of bug 253127).

  // nsIProgressEventSink

  onProgress: function(aRequest, aContext, aProgress, aProgressMax) {},
  onStatus: function(aRequest, aContext, aStatus, aStatusArg) {},

  /**
   * Initialize the resource from an existing DOM document object.
   * 
   * @param   document
   *          a DOM document object
   *
   */
  initFromDocument: function MSR_initFromDocument(document) {
    this._content = document;
    this._contentType = document.contentType;

    // Normally we set this property based on whether or not
    // XMLHttpRequest parsed the content into an XML document object,
    // but since we already have the content, we have to analyze
    // its content type ourselves to see if it is XML.
    this._isXML = (this.contentType == "text/xml" ||
                   this.contentType == "application/xml" ||
                   /^.+\/.+\+xml$/.test(this.contentType));
  },

  /**
   * Destroy references to avoid leak-causing cycles.  Instantiators must call
   * this method on all instances they instantiate once they're done with them.
   *
   */
  destroy: function MSR_destroy() {
    this._uri = null;
    this._content = null;
    this._loadCallback = null;
    this._errorCallback = null;
    this._loadTimer = null;
    this._httpAuthFailed = false;
    if (this._iframe) {
      if (this._iframe && this._iframe.parentNode)
        this._iframe.parentNode.removeChild(this._iframe);
      this._iframe = null;
    }
  },

  /**
   * Load the resource.
   * 
   * @param   loadCallback
   *          a function to invoke when the resource finishes loading
   * @param   errorCallback
   *          a function to invoke when an error occurs during the load
   *
   */
  load: function MSR_load(loadCallback, errorCallback) {
    LOG(this.uri.spec + " loading");
  
    this._loadCallback = loadCallback;
    this._errorCallback = errorCallback;

    var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
  
    var loadHandler = {
      _self: this,
      handleEvent: function MSR_loadHandler_handleEvent(event) {
        if (this._self._loadTimer)
          this._self._loadTimer.cancel();

        if (this._self._httpAuthFailed) {
          // Technically the request succeeded, but we treat it as a failure,
          // since we aren't able to handle HTTP authentication.
          LOG(this._self.uri.spec + " load failed; HTTP auth required");
          try     { this._self._handleError(event) }
          finally { this._self = null }
        }
        else if (event.target.channel.contentType == "multipart/x-mixed-replace") {
          // Technically the request succeeded, but we treat it as a failure,
          // since we aren't able to handle multipart content.
          LOG(this._self.uri.spec + " load failed; contains multipart content");
          try     { this._self._handleError(event) }
          finally { this._self = null }
        }
        else {
          LOG(this._self.uri.spec + " load succeeded; invoking callback");
          try     { this._self._handleLoad(event) }
          finally { this._self = null }
        }
      }
    };

    var errorHandler = {
      _self: this,
      handleEvent: function MSR_errorHandler_handleEvent(event) {
        if (this._self._loadTimer)
          this._self._loadTimer.cancel();

        LOG(this._self.uri.spec + " load failed");
        try     { this._self._handleError(event) }
        finally { this._self = null }
      }
    };

    // cancel loads that take too long
    // timeout specified in seconds at browser.microsummary.requestTimeout,
    // or 300 seconds (five minutes)
    var timeout = getPref("browser.microsummary.requestTimeout", 300) * 1000;
    var timerObserver = {
      _self: this,
      observe: function MSR_timerObserver_observe() {
        LOG("timeout loading microsummary resource " + this._self.uri.spec + ", aborting request");
        request.abort();
        try     { this._self.destroy() }
        finally { this._self = null }
      }
    };
    this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this._loadTimer.init(timerObserver, timeout, Ci.nsITimer.TYPE_ONE_SHOT);

    request = request.QueryInterface(Ci.nsIDOMEventTarget);
    request.addEventListener("load", loadHandler, false);
    request.addEventListener("error", errorHandler, false);
    
    request = request.QueryInterface(Ci.nsIXMLHttpRequest);
    request.open("GET", this.uri.spec, true);
    request.setRequestHeader("X-Moz", "microsummary");

    // Register ourselves as a listener for notification callbacks so we
    // can handle authorization requests and SSL issues like cert mismatches.
    // XMLHttpRequest will handle the notifications we don't handle.
    request.channel.notificationCallbacks = this;

    // If this is a bookmarked resource, and the bookmarks service recorded
    // its charset in the bookmarks datastore the last time the user visited it,
    // then specify the charset in the channel so XMLHttpRequest loads
    // the resource correctly.
    try {
      var resolver = Cc["@mozilla.org/embeddor.implemented/bookmark-charset-resolver;1"].
                     getService(Ci.nsICharsetResolver);
      if (resolver) {
        var charset = resolver.requestCharset(null, request.channel, {}, {});
        if (charset != "");
          request.channel.contentCharset = charset;
      }
    }
    catch(ex) {}

    request.send(null);
  },

  _handleLoad: function MSR__handleLoad(event) {
    var request = event.target;

    if (request.responseXML) {
      this._isXML = true;
      // XXX Figure out the parsererror format and log a specific error.
      if (request.responseXML.documentElement.nodeName == "parsererror")
        throw(request.channel.originalURI.spec + " contains invalid XML");
      this._content = request.responseXML;
      this._contentType = request.channel.contentType;
      this._loadCallback(this);
    }

    else if (request.channel.contentType == "text/html") {
      this._parse(request.responseText);
    }

    else {
      // This catches text/plain as well as any other content types
      // not accounted for by the content type-specific code above.
      this._content = request.responseText;
      this._contentType = request.channel.contentType;
      this._loadCallback(this);
    }
  },
  
  _handleError: function MSR__handleError(event) {
    // Call the error callback, then destroy ourselves to prevent memory leaks.
    try     { if (this._errorCallback) this._errorCallback() }
    finally { this.destroy() }
  },

  /**
   * Parse a string of HTML text.  Used by _load() when it retrieves HTML.
   * We do this via hidden XUL iframes, which according to bz is the best way
   * to do it currently, since bug 102699 is hard to fix.
   * 
   * @param   htmlText
   *          a string containing the HTML content
   *
   */
  _parse: function MSR__parse(htmlText) {
    // Find a window to stick our hidden iframe into.
    var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].
                         getService(Ci.nsIWindowMediator);
    var window = windowMediator.getMostRecentWindow("navigator:browser");
    // XXX We can use other windows, too, so perhaps we should try to get
    // some other window if there's no browser window open.  Perhaps we should
    // even prefer other windows, since there's less chance of any browser
    // window machinery like throbbers treating our load like one initiated
    // by the user.
    if (!window)
      throw(this._uri.spec + " can't parse; no browser window");
    var document = window.document;
    var rootElement = document.documentElement;
  
    // Create an iframe, make it hidden, and secure it against untrusted content.
    this._iframe = document.createElement('iframe');
    this._iframe.setAttribute("collapsed", true);
    this._iframe.setAttribute("type", "content");
  
    // Insert the iframe into the window, creating the doc shell.
    rootElement.appendChild(this._iframe);

    // When we insert the iframe into the window, it immediately starts loading
    // about:blank, which we don't need and could even hurt us (for example
    // by triggering bugs like bug 344305), so cancel that load.
    var webNav = this._iframe.docShell.QueryInterface(Ci.nsIWebNavigation);
    webNav.stop(Ci.nsIWebNavigation.STOP_NETWORK);

    // Turn off JavaScript and auth dialogs for security and other things
    // to reduce network load.
    // XXX We should also turn off CSS.
    this._iframe.docShell.allowJavascript = false;
    this._iframe.docShell.allowAuth = false;
    this._iframe.docShell.allowPlugins = false;
    this._iframe.docShell.allowMetaRedirects = false;
    this._iframe.docShell.allowSubframes = false;
    this._iframe.docShell.allowImages = false;
  
    var parseHandler = {
      _self: this,
      handleEvent: function MSR_parseHandler_handleEvent(event) {
        event.target.removeEventListener("DOMContentLoaded", this, false);
        try     { this._self._handleParse(event) }
        finally { this._self = null }
      }
    };
 
    // Convert the HTML text into an input stream.
    var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                    createInstance(Ci.nsIScriptableUnicodeConverter);
    converter.charset = "UTF-8";
    var stream = converter.convertToInputStream(htmlText);

    // Set up a channel to load the input stream.
    var channel = Cc["@mozilla.org/network/input-stream-channel;1"].
                  createInstance(Ci.nsIInputStreamChannel);
    channel.setURI(this._uri);
    channel.contentStream = stream;

    // Load in the background so we don't trigger web progress listeners.
    var request = channel.QueryInterface(Ci.nsIRequest);
    request.loadFlags |= Ci.nsIRequest.LOAD_BACKGROUND;

    // Specify the content type since we're not loading content from a server,
    // so it won't get specified for us, and if we don't specify it ourselves,
    // then Firefox will prompt the user to download content of "unknown type".
    var baseChannel = channel.QueryInterface(Ci.nsIChannel);
    baseChannel.contentType = "text/html";

    // Load as UTF-8, which it'll always be, because XMLHttpRequest converts
    // the text (i.e. XMLHTTPRequest.responseText) from its original charset
    // to UTF-16, then the string input stream component converts it to UTF-8.
    baseChannel.contentCharset = "UTF-8";

    // Register the parse handler as a load event listener and start the load.
    // Listen for "DOMContentLoaded" instead of "load" because background loads
    // don't fire "load" events.
    this._iframe.addEventListener("DOMContentLoaded", parseHandler, true);
    var uriLoader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader);
    uriLoader.openURI(channel, true, this._iframe.docShell);
  },

  /**
   * Handle a load event for the iframe-based parser.
   * 
   * @param   event
   *          the event object representing the load event
   *
   */
  _handleParse: function MSR__handleParse(event) {
    // XXX Make sure the parse was successful?

    this._content = this._iframe.contentDocument;
    this._contentType = this._iframe.contentDocument.contentType;
    this._loadCallback(this);
  }

};

/**
 * Get a resource currently loaded into a browser window.  Checks windows
 * one at a time, starting with the frontmost (a.k.a. most recent) one.
 * 
 * @param   uri
 *          the URI of the resource
 *
 * @returns a Resource object, if the resource is currently loaded
 *          into a browser window; otherwise null
 *
 */
function getLoadedMicrosummaryResource(uri) {
  var mediator = Cc["@mozilla.org/appshell/window-mediator;1"].
                 getService(Ci.nsIWindowMediator);

  // Apparently the Z order enumerator is broken on Linux per bug 156333.
  //var windows = mediator.getZOrderDOMWindowEnumerator("navigator:browser", true);
  var windows = mediator.getEnumerator("navigator:browser");

  while (windows.hasMoreElements()) {
    var win = windows.getNext();
    var tabBrowser = win.document.getElementById("content");
    for ( var i = 0; i < tabBrowser.browsers.length; i++ ) {
      var browser = tabBrowser.browsers[i];
      if (uri.equals(browser.currentURI)) {
        var resource = new MicrosummaryResource(uri);
        resource.initFromDocument(browser.contentDocument);
        return resource;
      }
    }
  }

  return null;
}

/**
 * Get a value from a pref or a default value if the pref doesn't exist.
 *
 * @param   prefName
 * @param   defaultValue
 * @returns the pref's value or the default (if it is missing)
 */
function getPref(prefName, defaultValue) {
  try {
    var prefBranch = Cc["@mozilla.org/preferences-service;1"].
                     getService(Ci.nsIPrefBranch);
    switch (prefBranch.getPrefType(prefName)) {
    case prefBranch.PREF_BOOL:
      return prefBranch.getBoolPref(prefName);
    case prefBranch.PREF_INT:
      return prefBranch.getIntPref(prefName);
    }
  }
  catch (ex) { /* return the default value */ }
  
  return defaultValue;
}


// From http://lxr.mozilla.org/mozilla/source/browser/components/search/nsSearchService.js

/**
 * Removes all characters not in the "chars" string from aName.
 *
 * @returns a sanitized name to be used as a filename, or a random name
 *          if a sanitized name cannot be obtained (if aName contains
 *          no valid characters).
 */
function sanitizeName(aName) {
  const chars = "-abcdefghijklmnopqrstuvwxyz0123456789";
  const maxLength = 60;

  var name = aName.toLowerCase();
  name = name.replace(/ /g, "-");
  //name = name.split("").filter(function (el) {
  //                               return chars.indexOf(el) != -1;
  //                             }).join("");
  var filteredName = "";
  for ( var i = 0 ; i < name.length ; i++ )
    if (chars.indexOf(name[i]) != -1)
      filteredName += name[i];
  name = filteredName;

  if (!name) {
    // Our input had no valid characters - use a random name
    for (var i = 0; i < 8; ++i)
      name += chars.charAt(Math.round(Math.random() * (chars.length - 1)));
  }

  if (name.length > maxLength)
    name = name.substring(0, maxLength);

  return name;
}





var gModule = {
  registerSelf: function(componentManager, fileSpec, location, type) {
    componentManager = componentManager.QueryInterface(Ci.nsIComponentRegistrar);
    
    for (var key in this._objects) {
      var obj = this._objects[key];
      componentManager.registerFactoryLocation(obj.CID,
                                               obj.className,
                                               obj.contractID,
                                               fileSpec,
                                               location,
                                               type);
    }
  },
  
  unregisterSelf: function(componentManager, fileSpec, location) {},

  getClassObject: function(componentManager, cid, iid) {
    if (!iid.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  
    for (var key in this._objects) {
      if (cid.equals(this._objects[key].CID))
      return this._objects[key].factory;
    }
    
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
  
  _objects: {
    service: {
      CID        : Components.ID("{460a9792-b154-4f26-a922-0f653e2c8f91}"),
      contractID : "@mozilla.org/microsummary/service;1",
      className  : "Microsummary Service",
      factory    : MicrosummaryServiceFactory = {
                     createInstance: function(aOuter, aIID) {
                       if (aOuter != null)
                         throw Components.results.NS_ERROR_NO_AGGREGATION;
                       var svc = new MicrosummaryService();
                       svc._init();
                      return svc.QueryInterface(aIID);
                     }
                   }
    }
  },
  
  canUnload: function(componentManager) {
    return true;
  }
};

function NSGetModule(compMgr, fileSpec) {
  return gModule;
}
