text: /**
   * @class MongoDbSupport
   * @memberof module:plugins
   * @description Allows to export rules as a MongoDB find object as well as populating the builder from a MongoDB object.
   */
  
  QueryBuilder.defaults({
      mongoOperators: {
          // @formatter:off
          equal:            function(v) { return v[0]; },
          not_equal:        function(v) { return { '$ne': v[0] }; },
          in:               function(v) { return { '$in': v }; },
          not_in:           function(v) { return { '$nin': v }; },
          less:             function(v) { return { '$lt': v[0] }; },
          less_or_equal:    function(v) { return { '$lte': v[0] }; },
          greater:          function(v) { return { '$gt': v[0] }; },
          greater_or_equal: function(v) { return { '$gte': v[0] }; },
          between:          function(v) { return { '$gte': v[0], '$lte': v[1] }; },
          not_between:      function(v) { return { '$lt': v[0], '$gt': v[1] }; },
          begins_with:      function(v) { return { '$regex': '^' + Utils.escapeRegExp(v[0]) }; },
          not_begins_with:  function(v) { return { '$regex': '^(?!' + Utils.escapeRegExp(v[0]) + ')' }; },
          contains:         function(v) { return { '$regex': Utils.escapeRegExp(v[0]) }; },
          not_contains:     function(v) { return { '$regex': '^((?!' + Utils.escapeRegExp(v[0]) + ').)*$', '$options': 's' }; },
          ends_with:        function(v) { return { '$regex': Utils.escapeRegExp(v[0]) + '$' }; },
          not_ends_with:    function(v) { return { '$regex': '(?<!' + Utils.escapeRegExp(v[0]) + ')$' }; },
          is_empty:         function(v) { return ''; },
          is_not_empty:     function(v) { return { '$ne': '' }; },
          is_null:          function(v) { return null; },
          is_not_null:      function(v) { return { '$ne': null }; }
          // @formatter:on
      },
  
      mongoRuleOperators: {
          $ne: function(v) {
              v = v.$ne;
              return {
                  'val': v,
                  'op': v === null ? 'is_not_null' : (v === '' ? 'is_not_empty' : 'not_equal')
              };
          },
          eq: function(v) {
              return {
                  'val': v,
                  'op': v === null ? 'is_null' : (v === '' ? 'is_empty' : 'equal')
              };
          },
          $regex: function(v) {
              v = v.$regex;
              if (v.slice(0, 4) == '^(?!' && v.slice(-1) == ')') {
                  return { 'val': v.slice(4, -1), 'op': 'not_begins_with' };
              }
              else if (v.slice(0, 5) == '^((?!' && v.slice(-5) == ').)*$') {
                  return { 'val': v.slice(5, -5), 'op': 'not_contains' };
              }
              else if (v.slice(0, 4) == '(?<!' && v.slice(-2) == ')$') {
                  return { 'val': v.slice(4, -2), 'op': 'not_ends_with' };
              }
              else if (v.slice(-1) == '$') {
                  return { 'val': v.slice(0, -1), 'op': 'ends_with' };
              }
              else if (v.slice(0, 1) == '^') {
                  return { 'val': v.slice(1), 'op': 'begins_with' };
              }
              else {
                  return { 'val': v, 'op': 'contains' };
              }
          },
          between: function(v) {
              return { 'val': [v.$gte, v.$lte], 'op': 'between' };
          },
          not_between: function(v) {
              return { 'val': [v.$lt, v.$gt], 'op': 'not_between' };
          },
          $in: function(v) {
              return { 'val': v.$in, 'op': 'in' };
          },
          $nin: function(v) {
              return { 'val': v.$nin, 'op': 'not_in' };
          },
          $lt: function(v) {
              return { 'val': v.$lt, 'op': 'less' };
          },
          $lte: function(v) {
              return { 'val': v.$lte, 'op': 'less_or_equal' };
          },
          $gt: function(v) {
              return { 'val': v.$gt, 'op': 'greater' };
          },
          $gte: function(v) {
              return { 'val': v.$gte, 'op': 'greater_or_equal' };
          }
      }
  });
  
  QueryBuilder.extend(/** @lends module:plugins.MongoDbSupport.prototype */ {
      /**
       * Returns rules as a MongoDB query
       * @param {object} [data] - current rules by default
       * @returns {object}
       * @fires module:plugins.MongoDbSupport.changer:getMongoDBField
       * @fires module:plugins.MongoDbSupport.changer:ruleToMongo
       * @fires module:plugins.MongoDbSupport.changer:groupToMongo
       * @throws UndefinedMongoConditionError, UndefinedMongoOperatorError
       */
      getMongo: function(data) {
          data = (data === undefined) ? this.getRules() : data;
  
          var self = this;
  
          return (function parse(group) {
              if (!group.condition) {
                  group.condition = self.settings.default_condition;
              }
              if (['AND', 'OR'].indexOf(group.condition.toUpperCase()) === -1) {
                  Utils.error('UndefinedMongoCondition', 'Unable to build MongoDB query with condition "{0}"', group.condition);
              }
  
              if (!group.rules) {
                  return {};
              }
  
              var parts = [];
  
              group.rules.forEach(function(rule) {
                  if (rule.rules && rule.rules.length > 0) {
                      parts.push(parse(rule));
                  }
                  else {
                      var mdb = self.settings.mongoOperators[rule.operator];
                      var ope = self.getOperatorByType(rule.operator);
                      var values = [];
  
                      if (mdb === undefined) {
                          Utils.error('UndefinedMongoOperator', 'Unknown MongoDB operation for operator "{0}"', rule.operator);
                      }
  
                      if (ope.nb_inputs !== 0) {
                          if (!(rule.value instanceof Array)) {
                              rule.value = [rule.value];
                          }
  
                          rule.value.forEach(function(v) {
                              values.push(Utils.changeType(v, rule.type, false));
                          });
                      }
  
                      /**
                       * Modifies the MongoDB field used by a rule
                       * @event changer:getMongoDBField
                       * @memberof module:plugins.MongoDbSupport
                       * @param {string} field
                       * @param {Rule} rule
                       * @returns {string}
                       */
                      var field = self.change('getMongoDBField', rule.field, rule);
  
                      var ruleExpression = {};
                      ruleExpression[field] = mdb.call(self, values);
  
                      /**
                       * Modifies the MongoDB expression generated for a rul
                       * @event changer:ruleToMongo
                       * @memberof module:plugins.MongoDbSupport
                       * @param {object} expression
                       * @param {Rule} rule
                       * @param {*} value
                       * @param {function} valueWrapper - function that takes the value and adds the operator
                       * @returns {object}
                       */
                      parts.push(self.change('ruleToMongo', ruleExpression, rule, values, mdb));
                  }
              });
  
              var groupExpression = {};
              groupExpression['$' + group.condition.toLowerCase()] = parts;
  
              /**
               * Modifies the MongoDB expression generated for a group
               * @event changer:groupToMongo
               * @memberof module:plugins.MongoDbSupport
               * @param {object} expression
               * @param {Group} group
               * @returns {object}
               */
              return self.change('groupToMongo', groupExpression, group);
          }(data));
      },
  
      /**
       * Converts a MongoDB query to rules
       * @param {object} query
       * @returns {object}
       * @fires module:plugins.MongoDbSupport.changer:parseMongoNode
       * @fires module:plugins.MongoDbSupport.changer:getMongoDBFieldID
       * @fires module:plugins.MongoDbSupport.changer:mongoToRule
       * @fires module:plugins.MongoDbSupport.changer:mongoToGroup
       * @throws MongoParseError, UndefinedMongoConditionError, UndefinedMongoOperatorError
       */
      getRulesFromMongo: function(query) {
          if (query === undefined || query === null) {
              return null;
          }
  
          var self = this;
  
          /**
           * Custom parsing of a MongoDB expression, you can return a sub-part of the expression, or a well formed group or rule JSON
           * @event changer:parseMongoNode
           * @memberof module:plugins.MongoDbSupport
           * @param {object} expression
           * @returns {object} expression, rule or group
           */
          query = self.change('parseMongoNode', query);
  
          // a plugin returned a group
          if ('rules' in query && 'condition' in query) {
              return query;
          }
  
          // a plugin returned a rule
          if ('id' in query && 'operator' in query && 'value' in query) {
              return {
                  condition: this.settings.default_condition,
                  rules: [query]
              };
          }
  
          var key = andOr(query);
          if (!key) {
              Utils.error('MongoParse', 'Invalid MongoDB query format');
          }
  
          return (function parse(data, topKey) {
              var rules = data[topKey];
              var parts = [];
  
              rules.forEach(function(data) {
                  // allow plugins to manually parse or handle special cases
                  data = self.change('parseMongoNode', data);
  
                  // a plugin returned a group
                  if ('rules' in data && 'condition' in data) {
                      parts.push(data);
                      return;
                  }
  
                  // a plugin returned a rule
                  if ('id' in data && 'operator' in data && 'value' in data) {
                      parts.push(data);
                      return;
                  }
  
                  var key = andOr(data);
                  if (key) {
                      parts.push(parse(data, key));
                  }
                  else {
                      var field = Object.keys(data)[0];
                      var value = data[field];
  
                      var operator = determineMongoOperator(value, field);
                      if (operator === undefined) {
                          Utils.error('MongoParse', 'Invalid MongoDB query format');
                      }
  
                      var mdbrl = self.settings.mongoRuleOperators[operator];
                      if (mdbrl === undefined) {
                          Utils.error('UndefinedMongoOperator', 'JSON Rule operation unknown for operator "{0}"', operator);
                      }
  
                      var opVal = mdbrl.call(self, value);
  
                      var id = self.getMongoDBFieldID(field, value);
  
                      /**
                       * Modifies the rule generated from the MongoDB expression
                       * @event changer:mongoToRule
                       * @memberof module:plugins.MongoDbSupport
                       * @param {object} rule
                       * @param {object} expression
                       * @returns {object}
                       */
                      var rule = self.change('mongoToRule', {
                          id: id,
                          field: field,
                          operator: opVal.op,
                          value: opVal.val
                      }, data);
  
                      parts.push(rule);
                  }
              });
  
              /**
               * Modifies the group generated from the MongoDB expression
               * @event changer:mongoToGroup
               * @memberof module:plugins.MongoDbSupport
               * @param {object} group
               * @param {object} expression
               * @returns {object}
               */
              return self.change('mongoToGroup', {
                  condition: topKey.replace('$', '').toUpperCase(),
                  rules: parts
              }, data);
          }(query, key));
      },
  
      /**
       * Sets rules a from MongoDB query
       * @see module:plugins.MongoDbSupport.getRulesFromMongo
       */
      setRulesFromMongo: function(query) {
          this.setRules(this.getRulesFromMongo(query));
      },
  
      /**
       * Returns a filter identifier from the MongoDB field.
       * Automatically use the only one filter with a matching field, fires a changer otherwise.
       * @param {string} field
       * @param {*} value
       * @fires module:plugins.MongoDbSupport:changer:getMongoDBFieldID
       * @returns {string}
       * @private
       */
      getMongoDBFieldID: function(field, value) {
          var matchingFilters = this.filters.filter(function(filter) {
              return filter.field === field;
          });
  
          var id;
          if (matchingFilters.length === 1) {
              id = matchingFilters[0].id;
          }
          else {
              /**
               * Returns a filter identifier from the MongoDB field
               * @event changer:getMongoDBFieldID
               * @memberof module:plugins.MongoDbSupport
               * @param {string} field
               * @param {*} value
               * @returns {string}
               */
              id = this.change('getMongoDBFieldID', field, value);
          }
  
          return id;
      }
  });
  
  /**
   * Finds which operator is used in a MongoDB sub-object
   * @memberof module:plugins.MongoDbSupport
   * @param {*} value
   * @returns {string|undefined}
   * @private
   */
  function determineMongoOperator(value) {
      if (value !== null && typeof value == 'object') {
          var subkeys = Object.keys(value);
  
          if (subkeys.length === 1) {
              return subkeys[0];
          }
          else {
              if (value.$gte !== undefined && value.$lte !== undefined) {
                  return 'between';
              }
              if (value.$lt !== undefined && value.$gt !== undefined) {
                  return 'not_between';
              }
              else if (value.$regex !== undefined) { // optional $options
                  return '$regex';
              }
              else {
                  return;
              }
          }
      }
      else {
          return 'eq';
      }
  }
  
  /**
   * Returns the key corresponding to "$or" or "$and"
   * @memberof module:plugins.MongoDbSupport
   * @param {object} data
   * @returns {string}
   * @private
   */
  function andOr(data) {
      var keys = Object.keys(data);
  
      for (var i = 0, l = keys.length; i < l; i++) {
          if (keys[i].toLowerCase() == '$or' || keys[i].toLowerCase() == '$and') {
              return keys[i];
          }
      }
  
      return undefined;
  }
  