function DataRegistry() {
  this.data = [];
  this.requiredFields = [];
  this.dataById = {};
}
DataRegistry.prototype = {
  /**
   * Throw an exception unconditionally if the first argument is false.
   *
   * @param mustBeTrue JSBool value to check.
   * @param errMsg JSString error message.
   *
   * @note Unlike assert, this is not debug code; failures must throw.
   */
  check: function check(mustBeTrue, errMsg) {
    if (!mustBeTrue)
      throw new Error(errMsg);
  },

  addEntry: function addEntry(aEntryObject) {
    var errMsg = "Required field missing from entry: ";
    for each (var i in this.requiredFields) {
      this.check(typeof aEntryObject[i] != "undefined", errMsg + i);
    }

    if (typeof this.validateAddEntry == "function") {
      var rv = this.validateAddEntry(aEntryObject);
      this.check(rv, "validateAddEntry rejected this entry.");
    }

    aEntryObject._dataIndex = this.data.length;
    this.data[this.data.length] = aEntryObject;
    if (("id" in aEntryObject) && aEntryObject.id) {
      errMsg = "Data registry already has an object with id " + aEntryObject.id;
      this.check(typeof this.dataById[aEntryObject.id] == "undefined", errMsg);
      this.dataById[aEntryObject.id] = aEntryObject;
    }

    if (typeof this.onEntryAdded == "function")
      this.onEntryAdded(aEntryObject);
  },

  addEntryByArguments: function addEntryByArguments(aName, aValue) {
    var obj = {};
    for (var i = 0; i < arguments.length; i += 2) {
      this.check(!(arguments[i] in obj), "No duplicate property names!");
      obj[arguments[i]] = arguments[i + 1];
    }
    this.addEntry(obj);
  },

  removeEntry: function removeEntry(aEntryObject) {
    this.removeIndex(aEntryObject._dataIndex);
  },

  removeIndex: function removeIndex(aIndex) {
    var errMsg = "removeIndex accepts only non-negative integers for arguments.";
    this.check(i >= 0 && i < this.data.length && Math.floor(i) == i, errMsg);

    if (typeof this.validateRemoveIndex == "function") {
      var rv = this.validateRemoveIndex(aIndex);
      this.check(rv, "validateRemoveIndex rejected this entry.");
    }

    this.data[aIndex] = null;
    if (("id" in aEntryObject) && aEntryObject.id) {
      this.dataById[aEntryObject.id] = null;
      delete this.dataById[aEntryObject.id];
    }

    if (typeof this.onEntryRemoved == "function")
      this.onEntryRemoved(aEntryObject);
  },

  testObject: function testObject(aObj, aSearchParams) {
    for (i in aSearchParams) {
      if (!i in aObj) {
        return false;
      }

      var param = aSearchParams[i];
      if ((param instanceof RegExp) && (typeof aObj[i] == "string")) {
        if (!param.test(aObj[i])) {
          return false;
        }
      } else if (param !== aObj[i]) {
        return false;
      }
    }
    return true;
  },

  searchFirstEntry: function searchFirstEntry(aSearchParams) {
    for (var i in aSearchParams) {
      this.check(aSearchParams[i], "Search field without parameter: " + i);
    }

    if ("id" in aSearchParams) {
      if (aSearchParams.id in this.dataById)
        return this.dataById[aSearchParams.id];

      return null;
    }

    for each (var obj in this.data) {
      if (this.testObject(obj, aSearchParams))
        return obj;
    }

    return null;
  },

  searchFirstIndex: function searchFirstIndex(aSearchParams) {
    for (var i in aSearchParams) {
      this.check(aSearchParams[i], "Search field without parameter: " + i);
    }

    if ("id" in aSearchParams) {
      if (aSearchParams.id in this.dataById)
        return this.dataById[aSearchParams.id];

      return null;
    }

    for (var index = 0; index < this.data.length; index++) {
      if (this.testObject(this.data[index], aSearchParams))
        return index;
    }

    return -1;
  },

  searchEntries: function searchEntries(aSearchParams) {
    for (var i in aSearchParams) {
      this.check(aSearchParams[i], "Search field without parameter: " + i);
    }

    if ("id" in aSearchParams) {
      if (aSearchParams.id in this.dataById)
        return this.dataById[aSearchParams.id];

      return null;
    }

    var response = [];

    for each (var obj in this.data) {
      if (this.testObject(obj, aSearchParams))
        response[response.length] = obj;
    }

    return (response.length ? response : null);
  },

  searchEntriesIndex: function searchEntries(aSearchParams) {
    for (var i in aSearchParams) {
      this.check(aSearchParams[i], "Search field without parameter: " + i);
    }

    if ("id" in aSearchParams) {
      if (aSearchParams.id in this.dataById)
        return this.dataById[aSearchParams.id];

      return null;
    }

    var response = [];

    for (var index = 0; index < this.data.length; index++) {
      if (this.testObject(this.data[index], aSearchParams))
        response[response.length] = index;
    }

    return (response.length ? response : null);
  },

  addProperties: function addProperties(aObj) {
    for (var prop in aObj) {
      this[prop] = aObj[prop];
    }
  },

  toString: function toString() {
    return "[object DataRegistry]";
  }
}