content: package net.csdn.mongo;
  
  import com.mongodb.BasicDBObject;
  import com.mongodb.DBCollection;
  import com.mongodb.DBCursor;
  import com.mongodb.DBObject;
  import net.csdn.common.collections.WowCollections;
  
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  
  import static net.csdn.common.collections.WowCollections.*;
  import static net.csdn.common.reflect.ReflectHelper.staticMethod;
  
  /**
   * User: WilliamZhu
   * Date: 12-10-17
   * Time: 下午4:57
   * <p/>
   * The +Criteria+ class is the core object needed in MongoSupport to retrieve
   * objects from the database. It is a DSL that essentially sets up the
   * selector and options arguments that get passed on to a <tt>Mongo Collection</tt>
   * in the Java driver. Each method on the +Criteria+ returns self to they
   * can be chained in order to create a readable criterion to be executed
   * against the database.
   * <p/>
   * Example setup:
   * <p/>
   * ```java
   * Criteria criteria = new Criteria()
   * ```
   * <p/>
   * ```java
   * criteria.select("filed").where(map("field","value")).skip(20).limit(20)
   * ```
   * <p/>
   * ```java
   * <tt>criteria.execute()</tt>
   * ```
   */
  public class Criteria {
      // attr_reader :klass, :options, :selector
      private Class<Document> kclass;
      private String tableName;
      private Map options = map();
      private Map selector = map();
  
      private final static String AGGREGATE_REDUCE = "function(obj, prev) { prev.count++; }";
  
  
      public Criteria aggregate() {
          // ReflectHelper.method("", "");
          return this;
      }
  
  
      public Criteria not(Map attributes) {
          updateSelector(attributes, "$ne");
          return this;
      }
  
  
      public Criteria where(Map _selector) {
          selector.putAll(_selector);
          return this;
      }
  
      public Criteria where(String _selector) {
          selector.put("$where", _selector);
          return this;
      }
  
      /*
      # Adds a criterion to the +Criteria+ that specifies values that must all
      # be matched in order to return results. Similar to an "in" clause but the
      # underlying conditional logic is an "AND" and not an "OR". The MongoDB
      # conditional operator that will be used is "$all".
      #
      # Options:
      #
      # attributes: A +Hash+ where the key is the field name and the value is an
      # +Array+ of values that must all match.
      #
      # Example:
      #
      # <tt>criteria.all(map("field",list("value1","value2")))</tt>
      #
      # <tt>criteria.all(map("field",list("value1","value2"),"field1",list("value3")))</tt>
      #
      # Returns: <tt>self</tt>
       */
      public Criteria all(Map _selector) {
          updateSelector(_selector, "$all");
          return this;
      }
  
      public <T> List<T> find(List list) {
          return in(map("_id", list)).fetch();
      }
  
      public <T> List<T> findAll() {
          return all(map()).fetch();
      }
  
      public <T> T findById(Object id) {
          return where(map("_id", id)).singleFetch();
      }
  
      /*
      # Adds a criterion to the +Criteria+ that specifies values that must
      # be matched in order to return results. This is similar to a SQL "WHERE"
      # clause. This is the actual selector that will be provided to MongoDB,
      # similar to the Javascript object that is used when performing a find()
      # in the MongoDB console.
      #
      # Options:
      #
      # _selectior: A +Hash+ that must match the attributes of the +Document+.
      #
      # Example:
      #
      # <tt>criteria.and(map("filed","value","field1","value1"))</tt>
      #
      # Returns: <tt>self</tt>
       */
      public Criteria and(Map _selector) {
          return where(_selector);
      }
  
  
      public int count() {
          return collection().find(translateMapToDBObject(selector)).count();
      }
  
      public int length() {
          return count();
      }
  
      //Update attributes of the first matching net.csdn.application.document.
      public void update(Map obj) {
          collection().update(translateMapToDBObject(selector), translateMapToDBObject(map("$set", obj)));
      }
  
      public void updateMulti(Map obj) {
          innerUpdate("$set", obj);
      }
  
      public void addToSet(Map obj) {
          innerUpdate("$addToSet", obj);
      }
  
      public void inc(Map obj) {
          innerUpdate("$inc", obj);
      }
  
      public void pop(Map obj) {
          innerUpdate("$pop", obj);
      }
  
      public void pull(Map obj) {
          innerUpdate("$pull", obj);
      }
  
      public void pullAll(Map obj) {
          innerUpdate("$pullAll", obj);
      }
  
      public void push(Map obj) {
          innerUpdate("$push", obj);
      }
  
      public void pushAll(Map obj) {
          innerUpdate("$pushAll", obj);
      }
  
      public void rename(Map obj) {
          innerUpdate("$rename", obj);
      }
  
      public void set(Map obj) {
          innerUpdate("$set", obj);
      }
  
      public void unset(Map obj) {
          innerUpdate("$unset", obj);
      }
  
      public void remove() {
          collection().findAndRemove(translateMapToDBObject(selector));
      }
  
  
      private void innerUpdate(String operateType, Map obj) {
          collection().updateMulti(translateMapToDBObject(selector), translateMapToDBObject(map(operateType, obj)));
      }
  
  
      public Criteria in(Map _selector) {
          updateSelector(_selector, "$in");
          return this;
      }
  
  
      public Criteria id(Object id) {
          selector.put("_id", id);
          return this;
      }
  
  
      //    public Criteria mapReduce(){
  //        collection().ma
  //    }
      /*
      # Adds a criterion to the +Criteria+ that specifies values where none
      # should match in order to return results. This is similar to an SQL "NOT IN"
      # clause. The MongoDB conditional operator that will be used is "$nin".
      #
      # Options:
      #
      # exclusions: A +Hash+ where the key is the field name and the value is an
      # +Array+ of values that none can match.
      #
      # Example:
      #
      # <tt>criteria.notIn(map("field",list("value","value1")))</tt>
      #
      # <tt>criteria.notIn(map("field",list("value","value1"),"filed1",list("value2","value3")))</tt>
      #
      # Returns: <tt>self</tt>
       */
      public Criteria notIn(Map _selector) {
          updateSelector(_selector, "$nin");
          return this;
      }
  
  
      /*
      # Adds a criterion to the +Criteria+ that finally you  fetch data from mongodb
      #
      #
      # Example:
      #
      # <tt>Person person = criteria.singleFetch()</tt>
      #
      #
      # Returns: <tt>subclass of net.csdn.application.document</tt>
       */
      public <T> T singleFetch() {
  
  
          options.put("limit", 1);
  
          List<T> objects = fetch();
  
          if (isEmpty(objects)) return null;
          return objects.get(0);
      }
  
      public boolean exists() {
          return count() > 0;
      }
  
      private void processSelector() {
  
          if (kclass != null) {
              selector = (Map) staticMethod(kclass, "translateKeyForParams", selector);
  
          }
      }
  
      private void init() {
          if (kclass != null) {
              collection = (DBCollection) staticMethod(kclass, "collection");
          }
          if (collection == null && !isEmpty(tableName)) collection = Document.mongoMongo.collection(tableName);
      }
  
      public List fetch() {
  
          processOptions();
          processSelector();
  
          List result = list();
  
          Map sort = (Map) options.get("sort");
          Integer skip = (Integer) options.get("skip");
          Integer limit = (Integer) options.get("limit");
          Map fields = (Map) options.get("fields");
  
          DBCursor dbCursor = fields == null
                  ? collection().find(translateMapToDBObject(selector))
                  : collection().find(translateMapToDBObject(selector), translateMapToDBObject(fields));
  
          if (sort != null)
              dbCursor.sort(translateMapToDBObject(sort));
  
          if (skip != null)
              dbCursor.skip(skip);
  
          if (limit != null)
              dbCursor.limit(limit);
  
          try {
              while (dbCursor.hasNext()) {
                  DBObject dbObject = dbCursor.next();
                  if (kclass == null) {
                      result.add(dbObject.toMap());
                  } else {
                      result.add(staticMethod(kclass, "create", dbObject.toMap()));
                  }
  
              }
          } finally {
              dbCursor.close();
          }
          return result;
      }
  
  
      public Criteria select(List fieldNames) {
          options.put("fields", fieldNames);
          return this;
      }
  
      public Criteria order(Map orderBy) {
          options.put("sort", orderBy);
          return this;
      }
  
      public Criteria skip(int skip) {
          options.put("skip", skip);
          return this;
      }
  
      public Criteria limit(int limit) {
          options.put("limit", limit);
          return this;
      }
  
  
      public <T> T first() {
          return singleFetch();
      }
  
      public <T> T one() {
          return first();
      }
  
      private static Map<String, Integer> SortMap = map(
              "asc", 1,
              "desc", -1
      );
  
  
      public <T> T last() {
          Map<String, Object> sorting = (Map) options.get("sort");
          if (sorting == null) sorting = map("_id", SortMap.get("desc"));
          options.put("sort", sorting);
          options.put("limit", 1);
          List result = fetch();
          if (result == null) return null;
          return (T) (result.get(0));
      }
  
      private Criteria updateSelector(Map attributes, String operator) {
          for (Object key : attributes.keySet()) {
              Object value = attributes.get(key);
              selector.put(key, map(operator, value));
          }
          return this;
      }
  
      private DBObject translateMapToDBObject(Map map) {
          return Document.translateMapToDBObject(map);
      }
  
      private DBCollection collection;
  
      public DBCollection collection() {
          return collection;
      }
  
      public Criteria(Class<Document> kclass) {
          this.kclass = kclass;
          init();
      }
  
      public Criteria(String tableName) {
          this.tableName = tableName;
          init();
      }
  
  
      private DBObject grouped(String start, String field, String reduce) {
          return collection().group(null, translateMapToDBObject(selector), new BasicDBObject("start", "start"), reduce.replaceAll("[field]", field));
      }
  
  
      private Map processOptions() {
  
          List<String> fields = (List) options.remove("fields");
          if (fields != null) {
              Map fieldsMap = new HashMap();
              for (String str : fields) {
                  fieldsMap.put(str, 1);
              }
              options.put("fields", fieldsMap);
          }
  
          Map<String, Object> sorting = (Map) options.get("sort");
          if (sorting != null) {
              Map<String, Object> newSorting = map();
              for (Map.Entry<String, Object> wow : sorting.entrySet()) {
  
                  Object value = wow.getValue();
                  if (value.equals("asc") || value.equals("desc")) {
                      newSorting.put(wow.getKey(), SortMap.get(value));
                  } else {
                      newSorting.put(wow.getKey(), value);
                  }
              }
              options.put("sort", newSorting);
          }
  
  
          return options;
      }
  
      /*
     # Filters the unused options out of the options +Hash+. Currently this
     # takes into account the "page" and "per_page" options that would be passed
     # in if using Paginator
      */
  
      private void filterOptions() {
          Integer page_num = (Integer) options.remove("page");
          Integer per_page_num = (Integer) options.remove("per_page");
          if (page_num != null || per_page_num != null) {
              if (per_page_num == null) per_page_num = 20;
              if (page_num == null) page_num = 1;
              options.put("limit", per_page_num);
              options.put("skip", page_num * per_page_num - per_page_num);
          }
      }
  
  
  }
  