#21
authorVyacheslav Tyutyunkov <tyutyunkov@gmail.com>
Fri, 29 Mar 2013 09:10:00 +0000 (16:10 +0700)
committerVyacheslav Tyutyunkov <tyutyunkov@gmail.com>
Fri, 29 Mar 2013 09:10:00 +0000 (16:10 +0700)
14 files changed:
jejdb/src/java/org/ejdb/bson/BSON.java
jejdb/src/java/org/ejdb/bson/BSONDecoder.java
jejdb/src/java/org/ejdb/bson/BSONEncoder.java
jejdb/src/java/org/ejdb/bson/BSONObject.java
jejdb/src/java/org/ejdb/bson/types/ObjectId.java
jejdb/src/java/org/ejdb/bson/util/RegexFlag.java
jejdb/src/java/org/ejdb/driver/BSONQueryObject.java [new file with mode: 0644]
jejdb/src/java/org/ejdb/driver/EJDB.java
jejdb/src/java/org/ejdb/driver/EJDBCollection.java
jejdb/src/java/org/ejdb/driver/EJDBException.java
jejdb/src/java/org/ejdb/driver/EJDBQuery.java
jejdb/src/java/org/ejdb/driver/EJDBQueryBuilder.java [new file with mode: 0644]
jejdb/src/java/org/ejdb/driver/EJDBResultSet.java
jejdb/src/test/org/ejdb/driver/test/EJDBTest.java

index 9184f1d..79b9f7a 100644 (file)
@@ -3,6 +3,8 @@ package org.ejdb.bson;
 import java.io.OutputStream;
 
 /**
+ * Util class for encode/decode BSON objects
+ *
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
@@ -24,10 +26,16 @@ public final class BSON {
     private BSON() {
     }
 
+    /**
+     * Encode BSON object to plain byte array
+     */
     public static byte[] encode(BSONObject obj){
         return new BSONEncoder().encode(obj);
     }
 
+    /**
+     * Decode BSON object from plain byte array
+     */
     public static BSONObject decode(byte[] data) {
         return new BSONDecoder().decode(data);
     }
index 1b96e8d..5007838 100644 (file)
@@ -8,6 +8,8 @@ import java.util.Date;
 import java.util.regex.Pattern;
 
 /**
+ * Default BSON object decoder (from plain byte array to Java object)
+ *
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
@@ -15,7 +17,12 @@ class BSONDecoder {
 
     private InputBuffer input;
 
-    public BSONObject decode(byte[] data) {
+    /**
+     * Decode BSON object
+     *
+     * @throws IllegalStateException if other decoding process active with this decoder
+     */
+    public BSONObject decode(byte[] data) throws IllegalStateException {
         if (isBusy()) {
             throw new IllegalStateException("other decoding in process");
         }
@@ -31,6 +38,9 @@ class BSONDecoder {
         return result;
     }
 
+    /**
+     * @return <code>true</code> if decoder currently in use
+     */
     public boolean isBusy() {
         return input != null;
     }
index 5349497..ffd0a9c 100644 (file)
@@ -14,6 +14,8 @@ import java.util.concurrent.atomic.AtomicLong;
 import java.util.regex.Pattern;
 
 /**
+ * Default BSON object encoder (from Java object to plain byte array)
+ *
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
@@ -21,6 +23,11 @@ class BSONEncoder {
 
     private OutputBuffer output;
 
+    /**
+     * Encode BSON object
+     *
+     * @throws IllegalStateException if other encoding process active with this encoder
+     */
     public byte[] encode(BSONObject object) throws IllegalStateException {
         if (isBusy()) {
             throw new IllegalStateException("other encoding in process");
@@ -38,6 +45,9 @@ class BSONEncoder {
         return result;
     }
 
+    /**
+     * @return <code>true</code> if encoder currently in use
+     */
     public boolean isBusy() {
         return output != null;
     }
@@ -47,7 +57,7 @@ class BSONEncoder {
 
         output.writeInt(0);
 
-        for (String field : object.keySet()) {
+        for (String field : object.fields()) {
             writeField(field, object.get(field));
         }
 
index c909afc..1afb0bb 100644 (file)
@@ -3,41 +3,100 @@ package org.ejdb.bson;
 import org.ejdb.bson.types.ObjectId;
 
 import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
+ * BSON object.
+ *
+ * NOTE:
+ *  - {@link BSONObject#ID_KEY} must be valid {@link ObjectId}((@link ObjectId} instance or valid <code>byte[]</code> or <code>String</code>)
+ *
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
 public class BSONObject {
+    /**
+     * ID-field name
+     */
     public static final String ID_KEY = "_id";
 
-    private Map<String, Object> data;
+    protected Map<String, Object> data;
+    protected List<String> fields;
 
     {
         data = new HashMap<String, Object>();
+        fields = new ArrayList<String>();
     }
 
+    /**
+     * Constructs new BSON object
+     */
     public BSONObject() {
     }
 
+    /**
+     * Constructs new BSON object with specified id
+     */
     public BSONObject(ObjectId oid) {
         this(ID_KEY, oid);
     }
 
+    /**
+     * Constructs new BSON object with initial data.
+     * The same as:
+     * <code>
+     *     BSONObject obj = new BSONObject();
+     *     obj.put(key, value);
+     * </code>
+     */
     public BSONObject(String key, Object value) {
         this.put(key, value);
     }
 
+    /**
+     * Constructs new BSON object and init data from specified Map.
+     * The same as
+     * <code>
+     *     BSONObject obj = new BSONObject();
+     *     obj.putAll(data);
+     * </code>
+     */
     public BSONObject(Map<String, Object> data) {
         this.putAll(data);
     }
 
-    public Object put(String key, Object value) {
+    /**
+     * Constructs new BSON object as copy of other BSON object.
+     */
+    public BSONObject(BSONObject src) {
+        if (src != null) {
+            this.putAll(src);
+        }
+    }
+
+    protected Object registerField(String key, Object value) {
+        if (!data.containsKey(key)) {
+            fields.add(key);
+        }
+
+        data.put(key, value);
+        return value;
+    }
+
+    /**
+     * Add new key->value to BSON object.
+     *
+     * @return added value
+     * @throws IllegalArgumentException if not valid ObjectId data passed as _id ({@link BSONObject#ID_KEY} field.
+     */
+    public Object put(String key, Object value) throws IllegalArgumentException {
         if (ID_KEY.equals(key)) {
             if (value instanceof ObjectId) {
                 // noop
@@ -50,49 +109,89 @@ public class BSONObject {
             }
         }
 
-        return data.put(key, value);
+        return registerField(key, value);
     }
 
+    /**
+     * The same as {@link BSONObject#put(String, Object)} but return <code>this</code>
+     */
     public BSONObject append(String key, Object value) {
         this.put(key, value);
 
         return this;
     }
 
+    /**
+     * Adds key->value pair to BSON object from specified Map
+     */
     public void putAll(Map<String, Object> values) {
         for (Map.Entry<String, Object> entry : values.entrySet()) {
             put(entry.getKey(), entry.getValue());
         }
     }
 
+    /**
+     * Adds key->value pair to BSON object from other BSON object
+     */
     public void putAll(BSONObject object) {
-        this.putAll(object.asMap());
+        for (String field : object.fields) {
+            this.put(field, object.get(field));
+        }
     }
 
-    public Map<String, Object> asMap() {
-        return new HashMap<String, Object>(data);
+    /**
+     * @return fields in adding order
+     */
+    public List<String> fields() {
+        return Collections.unmodifiableList(fields);
     }
 
-    public Set<String> keySet() {
-        return new HashSet<String>(data.keySet());
+    /**
+     * @return id of BSON object (if specified)
+     */
+    public ObjectId getId() {
+        return (ObjectId) get(ID_KEY);
     }
 
+    /**
+     * @return value of specified field if exists, or <code>null</code> otherwise
+     */
     public Object get(String key) {
         return data.get(key);
     }
 
-    public ObjectId getId() {
-        return (ObjectId) get(ID_KEY);
-    }
-
+    /**
+     * @return fields count
+     */
     public int size() {
         return data.size();
     }
 
+    /**
+     * Checks field contains in BSON object
+     */
     public boolean containsField(String key) {
         return data.containsKey(key);
     }
 
+    /**
+     * Removes field from Object
+     */
+    public void remove(String field) {
+        if (data.containsKey(field)) {
+            fields.remove(field);
+            data.remove(field);
+        }
+    }
+
+    /**
+     * Removes all fields
+     */
+    public void clear() {
+        fields.clear();
+        data.clear();
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this != o && (null == o || !(o instanceof BSONObject))) {
index dcafd38..9245bdc 100644 (file)
@@ -3,6 +3,8 @@ package org.ejdb.bson.types;
 import java.util.Arrays;
 
 /**
+ * BSON Object ID
+ *
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
@@ -10,7 +12,12 @@ public class ObjectId {
 
     private byte[] data;
 
-    public ObjectId(byte[] data) {
+    /**
+     * Read ObjectId from byte array
+     *
+     * @throws IllegalStateException if not valid ObjectId data passed
+     */
+    public ObjectId(byte[] data) throws IllegalArgumentException {
         if (data == null || data.length != 12) {
             throw new IllegalArgumentException("unexpected ObjectId data");
         }
@@ -19,6 +26,11 @@ public class ObjectId {
         System.arraycopy(data, 0, this.data, 0, 12);
     }
 
+    /**
+     * Read ObjectId from string
+     *
+     * @throws IllegalStateException if not valid ObjectId data passed
+     */
     public ObjectId(String value) {
         if (!isValid(value)) {
             throw new IllegalArgumentException("unexpected ObjectId data");
@@ -30,6 +42,9 @@ public class ObjectId {
         }
     }
 
+    /**
+     * Export ObjectId to plain byte array
+     */
     public byte[] toByteArray() {
         byte[] result = new byte[12];
         System.arraycopy(data, 0, result, 0, 12);
@@ -59,6 +74,9 @@ public class ObjectId {
         return builder.toString();
     }
 
+    /**
+     * Checks string on valid ObjectId data
+     */
     public static boolean isValid(String value) {
         if (value == null || value.length() != 24) {
             return false;
index 40372f8..86c62ae 100644 (file)
@@ -9,6 +9,8 @@ import java.util.Map;
 import java.util.regex.Pattern;
 
 /**
+ * Util class for convert Java regex flags to BSON string and conversely
+ *
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
@@ -26,6 +28,9 @@ public final class RegexFlag {
         this.supported = supported;
     }
 
+    /**
+     * Convert Java regex flags to BSON string
+     */
     public static String regexFlagsToString(int flags) {
         StringBuilder result = new StringBuilder();
         for (RegexFlag rf : regexFlags) {
@@ -37,6 +42,9 @@ public final class RegexFlag {
         return result.toString();
     }
 
+    /**
+     * Read Java regex flags from BSON string
+     */
     public static int stringToRegexFlags(String str) {
         int flags = 0;
 
@@ -50,20 +58,32 @@ public final class RegexFlag {
         return flags;
     }
 
-    public static void registerRegexFlag(int flag, char character, boolean supported) {
+    /**
+     * Register flag conversation rules
+     */
+    protected static void registerRegexFlag(int flag, char character, boolean supported) {
         RegexFlag rf = new RegexFlag(flag, character, supported);
         regexFlags.add(rf);
         characterToRegexFlagMap.put(rf.getCharacter(), rf);
     }
 
+    /**
+     * @return Java flag
+     */
     public int getFlag() {
         return flag;
     }
 
+    /**
+     * @return BSON character for associated Java regex flag
+     */
     public char getCharacter() {
         return character;
     }
 
+    /**
+     * @return <code>true</code> if BSON supported current Java flag
+     */
     public boolean isSupported() {
         return supported;
     }
diff --git a/jejdb/src/java/org/ejdb/driver/BSONQueryObject.java b/jejdb/src/java/org/ejdb/driver/BSONQueryObject.java
new file mode 100644 (file)
index 0000000..890a31d
--- /dev/null
@@ -0,0 +1,52 @@
+package org.ejdb.driver;
+
+import org.ejdb.bson.BSONObject;
+import org.ejdb.bson.types.ObjectId;
+
+import java.util.Map;
+
+/**
+ * BSON object for EJDB queries (limitation checks for {@link BSONObject#ID_KEY} field)
+ *
+ * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
+ * @version $Id$
+ */
+public class BSONQueryObject extends BSONObject {
+
+    public BSONQueryObject() {
+        super();
+    }
+
+    public BSONQueryObject(String key, Object value) {
+        super(key, value);
+    }
+
+    public BSONQueryObject(Map<String, Object> data) {
+        super(data);
+    }
+
+    public BSONQueryObject(BSONObject src) {
+        super(src);
+    }
+
+    @Override
+    public Object put(String key, Object value) {
+        return registerField(key, value);
+    }
+
+    @Override
+    public BSONQueryObject append(String key, Object value) {
+        super.append(key, value);
+        return this;
+    }
+
+    /**
+     * BSON Query objects can not contains dedicated ObjectID
+     * @return
+     */
+    @Deprecated
+    @Override
+    public ObjectId getId() {
+        return null;
+    }
+}
index 176138e..2f2c0bc 100644 (file)
@@ -54,7 +54,7 @@ public class EJDB {
         System.loadLibrary("jejdb");
     }
 
-    private long dbPointer;
+    private transient long dbPointer;
 
     private String path;
     private Map<String, EJDBCollection> collections;
index 753673e..78b992e 100644 (file)
@@ -4,6 +4,7 @@ import org.ejdb.bson.BSONObject;
 import org.ejdb.bson.types.ObjectId;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 
@@ -339,24 +340,40 @@ public class EJDBCollection {
     }
 
     /**
+     * Creates new EJDB Query for current collection.
+     */
+    public EJDBQuery createQuery(EJDBQueryBuilder query) {
+        return new EJDBQuery(this, query);
+    }
+
+    /**
      * @see EJDBCollection#createQuery(org.ejdb.bson.BSONObject, org.ejdb.bson.BSONObject[], org.ejdb.bson.BSONObject)
+     * @deprecated
+     * @use EJDBCollection#createQuery(EJDBQueryBuilder)
      */
+    @Deprecated
     public EJDBQuery createQuery(BSONObject query) {
-        return new EJDBQuery(this, query, null, null);
+        return this.createQuery(new EJDBQueryBuilder(query, null, null));
     }
 
     /**
      * @see EJDBCollection#createQuery(org.ejdb.bson.BSONObject, org.ejdb.bson.BSONObject[], org.ejdb.bson.BSONObject)
+     * @deprecated
+     * @use EJDBCollection#createQuery(EJDBQueryBuilder)
      */
+    @Deprecated
     public EJDBQuery createQuery(BSONObject query, BSONObject[] qors) {
-        return new EJDBQuery(this, query, qors, null);
+        return this.createQuery(new EJDBQueryBuilder(query, qors != null ? Arrays.asList(qors) : null, null));
     }
 
     /**
      * @see EJDBCollection#createQuery(org.ejdb.bson.BSONObject, org.ejdb.bson.BSONObject[], org.ejdb.bson.BSONObject)
+     * @deprecated
+     * @use EJDBCollection#createQuery(EJDBQueryBuilder)
      */
+    @Deprecated
     public EJDBQuery createQuery(BSONObject query, BSONObject hints) {
-        return new EJDBQuery(this, query, null, hints);
+        return this.createQuery(new EJDBQueryBuilder(query, null, hints));
     }
 
     /**
@@ -367,10 +384,12 @@ public class EJDBCollection {
      * @param query Main BSON query object
      * @param qors  Array of additional OR query objects (joined with OR predicate).
      * @param hints BSON object with query hints.
-     * @return
+     * @deprecated
+     * @use EJDBCollection#createQuery(EJDBQueryBuilder)
      */
+    @Deprecated
     public EJDBQuery createQuery(BSONObject query, BSONObject[] qors, BSONObject hints) {
-        return new EJDBQuery(this, query, qors, hints);
+        return this.createQuery(new EJDBQueryBuilder(query, qors != null ? Arrays.asList(qors) : null, hints));
     }
 
     /**
index 47a1b5a..ae2e867 100644 (file)
@@ -4,9 +4,25 @@ package org.ejdb.driver;
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
-public class EJDBException extends Exception {
+public class EJDBException extends RuntimeException {
     private int code;
 
+    public EJDBException() {
+        super();
+    }
+
+    public EJDBException(String message) {
+        super(message);
+    }
+
+    public EJDBException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public EJDBException(Throwable cause) {
+        super(cause);
+    }
+
     public EJDBException(int code, String message) {
         super(message);
         this.code = code;
index 630b1b6..e16ba60 100644 (file)
@@ -11,6 +11,8 @@ import java.util.List;
 import java.util.Map;
 
 /**
+ * EJDB Query object.
+ *
  * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
  * @version $Id$
  */
@@ -20,28 +22,13 @@ public class EJDBQuery {
     protected static final int JBQRYCOUNT = 1;
 
     private EJDBCollection coll;
-    private BSONObject query;
-    private List<BSONObject> qors;
-    private BSONObject hints;
+    private EJDBQueryBuilder query;
     private int flags;
 
-    EJDBQuery(EJDBCollection coll, BSONObject query, BSONObject[] qors, BSONObject hints) {
+
+    EJDBQuery(EJDBCollection coll, EJDBQueryBuilder query) {
         this.coll = coll;
         this.query = query;
-        this.qors = new ArrayList<BSONObject>();
-        if (qors != null) {
-            this.qors.addAll(Arrays.asList(qors));
-        }
-        this.hints = hints;
-    }
-
-    /**
-     * Return main query object
-     *
-     * @return
-     */
-    public BSONObject getQueryObject() {
-        return query;
     }
 
     /**
@@ -60,7 +47,7 @@ public class EJDBQuery {
      * Execute query
      */
     public EJDBResultSet find(OutputStream log) throws EJDBException, IOException {
-        return this.execute(hints, 0, log).getResultSet();
+        return this.execute(query.getQueryHints(), 0, log).getResultSet();
     }
 
     /**
@@ -79,10 +66,9 @@ public class EJDBQuery {
      * Same as  {@link org.ejdb.driver.EJDBQuery#find()} but retrieves only one matching JSON object.
      */
     public BSONObject findOne(OutputStream log) throws EJDBException, IOException {
-        Map<String, Object> hintsMap = hints != null ? hints.asMap() : new HashMap();
-        hintsMap.put("$max", 1);
+        BSONQueryObject hints = new BSONQueryObject(query.getQueryHints()).append("$max", 1);
 
-        EJDBResultSet rs = this.execute(new BSONObject(hintsMap), 0, null).getResultSet();
+        EJDBResultSet rs = this.execute(hints, 0, null).getResultSet();
         BSONObject result = rs.hasNext() ? rs.next() : null;
         rs.close();
 
@@ -109,7 +95,7 @@ public class EJDBQuery {
      * @return count of affected objects
      */
     public int update(OutputStream log) throws EJDBException, IOException {
-        return this.execute(hints, JBQRYCOUNT, log).getCount();
+        return this.execute(query.getQueryHints(), JBQRYCOUNT, log).getCount();
     }
 
     /**
@@ -129,14 +115,11 @@ public class EJDBQuery {
      * Convenient count(*) operation
      */
     public int count(OutputStream log) throws EJDBException, IOException {
-        return this.execute(hints, JBQRYCOUNT, log).getCount();
+        return this.execute(query.getQueryHints(), JBQRYCOUNT, log).getCount();
     }
 
     protected QResult execute(BSONObject hints, int flags, OutputStream log) throws EJDBException, IOException {
-        BSONObject[] qors = new BSONObject[this.qors.size()];
-        this.qors.toArray(qors);
-
-        return this.execute(query, qors, hints, flags, log);
+        return this.execute(query.getMainQuery(), query.getOrQueries(), hints, flags, log);
     }
 
     protected native QResult execute(BSONObject query, BSONObject[] qors, BSONObject hints, int flags, OutputStream log) throws EJDBException, IOException;
diff --git a/jejdb/src/java/org/ejdb/driver/EJDBQueryBuilder.java b/jejdb/src/java/org/ejdb/driver/EJDBQueryBuilder.java
new file mode 100644 (file)
index 0000000..c56864d
--- /dev/null
@@ -0,0 +1,548 @@
+package org.ejdb.driver;
+
+import org.ejdb.bson.BSONObject;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Query/BSON builder is used to create EJDB queries.
+ * EJDBQueryBuilder can be used to construct BSON objects as well as queries.
+ *
+ * @author Tyutyunkov Vyacheslav (tve@softmotions.com)
+ * @version $Id$
+ */
+public class EJDBQueryBuilder {
+    private EJDBQueryBuilder parent;
+
+    private BSONObject query;
+    private List<BSONObject> queryOrs;
+    private BSONObject hints;
+
+    public EJDBQueryBuilder() {
+        query = new BSONQueryObject();
+        queryOrs = new ArrayList<BSONObject>();
+        hints = new BSONQueryObject();
+    }
+
+    public EJDBQueryBuilder(BSONObject query, List<BSONObject> queryOrs, BSONObject hints) {
+        this.query = query;
+        this.queryOrs = queryOrs != null ? queryOrs : new ArrayList<BSONObject>();
+        this.hints = hints != null ? hints : new BSONQueryObject();
+    }
+
+    protected EJDBQueryBuilder(EJDBQueryBuilder parent, BSONObject query) {
+        this.parent = parent;
+        this.query = query;
+    }
+
+    /**
+     * @return main BSON query object
+     */
+    public BSONObject getMainQuery() {
+        return query;
+    }
+
+    /**
+     * @return BSON objects for additional OR queries
+     */
+    public BSONObject[] getOrQueries() {
+        if (queryOrs == null) {
+            return new BSONObject[0];
+        }
+
+        int i = -1;
+        BSONObject[] ors = new BSONObject[queryOrs.size()];
+        for (BSONObject qor : queryOrs) {
+            ors[++i] = qor;
+        }
+
+        return ors;
+    }
+
+    /**
+     * @return BSON hints object
+     */
+    public BSONObject getQueryHints() {
+        return hints;
+    }
+
+    /**
+     * Adds query restrintions in main query object.
+     *
+     * @param field   field path
+     * @param value   field value
+     * @param replace if <code>true</code> all other restrictions will be replaces, otherwise trying to add restrictions for field
+     */
+    protected EJDBQueryBuilder addOperation(String field, Object value, boolean replace) {
+        if (!query.containsField(field) || replace || !(value instanceof BSONObject)) {
+            query.put(field, value);
+        } else {
+            Object cvalue = query.get(field);
+            if (cvalue instanceof BSONObject) {
+                ((BSONObject) cvalue).putAll((BSONObject) value);
+            } else {
+                query.put(field, value);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Checks hints section allowed.
+     *
+     * @throws EJDBException if hints section if not allowed for current EJDBQueryBuilder object
+     */
+    protected void checkHintsAvailable() throws EJDBException {
+        if (hints == null) {
+            throw new EJDBException("hints section not available for subquery such as or-query or element match query");
+        }
+    }
+
+    /**
+     * Adds pair name->value to hints BSON object.
+     *
+     * @throws EJDBException if hints section if not allowed for current EJDBQueryBuilder object
+     */
+    protected EJDBQueryBuilder addHint(String name, Object value) throws EJDBException {
+        checkHintsAvailable();
+        hints.put(name, value);
+        return this;
+    }
+
+    /**
+     * Adds field equality restriction.
+     * <p/>
+     * <code>query.field(field, value); // -> {field : value}</code>
+     */
+    public EJDBQueryBuilder field(String field, Object value) {
+        return this.field(field).eq(value);
+    }
+
+    /**
+     * Adds contraint for field
+     */
+    public Constraint field(String field) {
+        return new Constraint(field, false);
+    }
+
+    /**
+     * Element match construction.
+     * - $elemMatch The $elemMatch operator matches more than one component within an array element.
+     * - { array: { $elemMatch: { value1 : 1, value2 : { $gt: 1 } } } }
+     * <p/>
+     * Restriction: only one $elemMatch allowed in context of one array field.
+     */
+    public EJDBQueryBuilder elementMatch(String field) {
+        BSONQueryObject emqbson = new BSONQueryObject();
+        query.put(field, new BSONQueryObject("$elemMatch", emqbson));
+        return new EJDBQueryBuilder(null, emqbson);
+    }
+
+    /**
+     * Add <code>OR</code> joined query restrictions.
+     *
+     * @throws EJDBException if or section if not allowed for current EJDBQueryBuilder object (in ElementMatch-query, for example)
+     */
+    public EJDBQueryBuilder or() throws EJDBException {
+        if (parent != null) {
+            return parent.or();
+        }
+
+        if (queryOrs == null) {
+            throw new EJDBException("or section not available for subquery such as element match query");
+        }
+
+        BSONQueryObject qor = new BSONQueryObject();
+        queryOrs.add(qor);
+        return new EJDBQueryBuilder(this, qor);
+    }
+
+    /**
+     * Set specified fiels to value
+     * <p/>
+     * <code>query.set(field1, value1).set(field2, value2); // -> { ..., $set : {field1 : value1, field2 : value2}}</code>
+     */
+    public EJDBQueryBuilder set(String field, Object value) {
+        return new Constraint(field, new Constraint("$set", false)).addOperation(value);
+    }
+
+    /**
+     * Atomic upsert.
+     * If matching records are found it will be <code>$set</code> operation, otherwise new record will be inserted with field specified by value.
+     * <p/>
+     * <code>query.field(field, value).upsert(field, value); // -> {field : value, $upsert : {field : value}}</code>
+     */
+    public EJDBQueryBuilder upsert(String field, Object value) {
+        return new Constraint(field, new Constraint("$upsert", false)).addOperation(value);
+    }
+
+    /**
+     * Increment specified field. Only number types are supported.
+     * <p/>
+     * <code>query.int(field1, value1).int(field2, value2); // -> { ..., $int : {field1 : value1, field2 : value2}}</code>
+     */
+    public EJDBQueryBuilder inc(String field, Number inc) {
+        return new Constraint(field, new Constraint("$inc", false)).addOperation(inc);
+    }
+
+    /**
+     * In-place record removal operation.
+     * <p/>
+     * Example:
+     * Next update query removes all records with name eq 'andy':
+     * <code>query.field("name", "andy").dropAll()</code>
+     */
+    public EJDBQueryBuilder dropAll() {
+        return new Constraint("$dropAll").addOperation(true);
+    }
+
+    /**
+     * Atomically adds value to the array field only if value not in the array already. If containing array is missing it will be created.
+     */
+    public EJDBQueryBuilder addToSet(String field, Object value) {
+        return new Constraint(field, new Constraint("$addToSet", false)).addOperation(value);
+    }
+
+    /**
+     * Atomically performs <code>set union</code> with values in val for specified array field.
+     */
+    public EJDBQueryBuilder addToSetAll(String field, Object... values) {
+        return new Constraint(field, new Constraint("$addToSetAll", false)).addOperation((Object[]) values);
+    }
+
+    /**
+     * Atomically performs <code>set union</code> with values in val for specified array field.
+     */
+    public EJDBQueryBuilder addToSetAll(String field, Collection<Object> values) {
+        Object[] objects = new Object[values.size()];
+        values.toArray(objects);
+        return new Constraint(field, new Constraint("$addToSetAll", false)).addOperation(objects);
+    }
+
+    /**
+     * Atomically removes all occurrences of value from field, if field is an array.
+     */
+    public EJDBQueryBuilder pull(String field, Object value) {
+        return new Constraint(field, new Constraint("$pull", false)).addOperation(value);
+    }
+
+    /**
+     * Atomically performs <code>set substraction</code> of values for specified array field.
+     */
+    public EJDBQueryBuilder pullAll(String field, Object... values) {
+        return new Constraint(field, new Constraint("$pullAll", false)).addOperation((Object[]) values);
+    }
+
+    /**
+     * Atomically performs <code>set substraction</code> of values for specified array field.
+     */
+    public EJDBQueryBuilder pullAll(String field, Collection<Object> values) {
+        Object[] objects = new Object[values.size()];
+        values.toArray(objects);
+        return new Constraint(field, new Constraint("$pullAll", false)).addOperation(objects);
+    }
+
+    /**
+     * Make {@se http://github.com/Softmotions/ejdb/wiki/Collection-joins collection join} for select queries.
+     */
+    public EJDBQueryBuilder join(String fpath, String collname) {
+        return new Constraint("$join", new Constraint(fpath, new Constraint("$do", false))).addOperation(collname);
+    }
+
+    /**
+     * Sets max number of records in the result set.
+     */
+    public EJDBQueryBuilder setMaxResults(int maxResults) {
+        return addHint("$max", maxResults);
+    }
+
+    /**
+     * Sets number of skipped records in the result set.
+     */
+    public EJDBQueryBuilder setOffset(int offset) {
+        return addHint("$skip", offset);
+    }
+
+    /**
+     * Sets fields to be included or exluded in resulting objects.
+     * If field presented in <code>$orderby</code> clause it will be forced to include in resulting records.
+     */
+    public EJDBQueryBuilder setFieldIncluded(String field, boolean incldue) {
+        checkHintsAvailable();
+
+        BSONObject fields = hints.containsField("$fields") && (hints.get("$fields") instanceof BSONObject) ? (BSONObject) hints.get("$fields") : new BSONQueryObject();
+        fields.put(field, incldue ? 1 : -1);
+        hints.put("$fields", fields);
+
+        return this;
+    }
+
+    /**
+     * Sets fields to be included in resulting objects.
+     * If field presented in <code>$orderby</code> clause it will be forced to include in resulting records.
+     */
+    public EJDBQueryBuilder includeField(String field) {
+        return setFieldIncluded(field, true);
+    }
+
+    /**
+     * Sets fields to be excluded from resulting objects.
+     * If field presented in <code>$orderby</code> clause it will be forced to include in resulting records.
+     */
+    public EJDBQueryBuilder excludeField(String field) {
+        return setFieldIncluded(field, false);
+    }
+
+    /**
+     * Resturs return sorting rules control object
+     */
+    public OrderBy orderBy() {
+        checkHintsAvailable();
+
+        if (hints.containsField("$orderby") && hints.get("$orderby") != null && (hints.get("$orderby") instanceof BSONObject)) {
+            return new OrderBy((BSONObject) hints.get("$orderby"));
+        } else {
+            return new OrderBy((BSONObject) hints.put("$orderby", new BSONQueryObject()));
+        }
+    }
+
+    /**
+     * Find constraint for specified field
+     */
+    public class Constraint {
+        private String field;
+        private boolean replace;
+        private Constraint parent;
+
+        protected Constraint(String field) {
+            this(field, true);
+        }
+
+        protected Constraint(String field, boolean replace) {
+            this.field = field;
+            this.replace = replace;
+        }
+
+        protected Constraint(String field, Constraint parent) {
+            this.field = field;
+            this.parent = parent;
+            this.replace = true;
+        }
+
+        /**
+         * Add curently restrictons tree to query
+         */
+        protected EJDBQueryBuilder addOperation(Object value) {
+            if (parent != null) {
+                return parent.addOperation(new BSONQueryObject(field, value));
+            } else {
+                return EJDBQueryBuilder.this.addOperation(field, value, replace);
+            }
+        }
+
+        /**
+         * Add <code>$not</code> negatiation contraint
+         * <p/>
+         * Example:
+         * <code>query.field(field).not().eq(value); // {field : { $not : value }}</code>
+         * <code>query.field(field).not().bt(start, end); // {field : { $not : {$bt : [start, end]}}}</code>
+         */
+        public Constraint not() {
+            return new Constraint("$not", this);
+        }
+
+        /**
+         * Field equality restriction.
+         * All usage samples represent same thing: {"field" : value}
+         * <p/>
+         * Example:
+         * <code>query.field(field, value); // -> {field : value}</code>
+         * <code>query.field(field).eq(value); // -> {field : value}</code>
+         */
+        public EJDBQueryBuilder eq(Object value) {
+            return this.addOperation(value);
+        }
+
+        /**
+         * Greater than or equal value (field_value >= value)
+         */
+        public EJDBQueryBuilder gte(Number value) {
+            return new Constraint("$gte", this).addOperation(value);
+        }
+
+        /**
+         * Greater than value (field_value > value)
+         */
+        public EJDBQueryBuilder gt(Number value) {
+            return new Constraint("$gt", this).addOperation(value);
+        }
+
+        /**
+         * Lesser then or equal value (field_value <= value)
+         */
+        public EJDBQueryBuilder lte(Number value) {
+            return new Constraint("$lte", this).addOperation(value);
+        }
+
+        /**
+         * Lesser then value (field_value < value)
+         */
+        public EJDBQueryBuilder lt(Number value) {
+            return new Constraint("$lt", this).addOperation(value);
+        }
+
+        /**
+         * Between number (start <= field_value <= end)
+         */
+        public EJDBQueryBuilder bt(Number start, Number end) {
+            return new Constraint("$bt", this).addOperation(new Number[]{start, end});
+        }
+
+        /**
+         * Field value matched <b>any</b> value of specified in values.
+         */
+        public EJDBQueryBuilder in(Object... values) {
+            return new Constraint("$in", this).addOperation((Object[]) values);
+        }
+
+        /**
+         * Field value matched <b>any</b> value of specified in values.
+         */
+        public EJDBQueryBuilder in(Collection<Object> values) {
+            Object[] objects = new Object[values.size()];
+            values.toArray(objects);
+            return new Constraint("$in", this).addOperation(objects);
+        }
+
+        /**
+         * Negation of {@link Constraint#in(Object...)}
+         */
+        public EJDBQueryBuilder notIn(Object... values) {
+            return new Constraint("$nin", this).addOperation((Object[]) values);
+        }
+
+        /**
+         * Negation of {@link Constraint#in(java.util.Collection)}
+         */
+        public EJDBQueryBuilder notIn(Collection<Object> values) {
+            Object[] objects = new Object[values.size()];
+            values.toArray(objects);
+            return new Constraint("$nin", this).addOperation(objects);
+        }
+
+        /**
+         * Strins starts with prefix
+         */
+        public EJDBQueryBuilder begin(String value) {
+            return new Constraint("$begin", this).addOperation(value);
+        }
+
+        /**
+         * String tokens (or string array vals) matches <b>all</b> tokens in specified array.
+         */
+        public EJDBQueryBuilder strAnd(String... values) {
+            return new Constraint("$strand", this).addOperation((String[]) values);
+        }
+
+        /**
+         * String tokens (or string array vals) matches <b>all</b> tokens in specified collection.
+         */
+        public EJDBQueryBuilder strAnd(Collection<String> values) {
+            String[] strs = new String[values.size()];
+            values.toArray(strs);
+            return new Constraint("$strand", this).addOperation(strs);
+        }
+
+        /**
+         * String tokens (or string array vals) matches <b>any</b> tokens in specified array.
+         */
+        public EJDBQueryBuilder strOr(String... values) {
+            return new Constraint("$stror", this).addOperation((String[]) values);
+        }
+
+        /**
+         * String tokens (or string array vals) matches <b>any</b> tokens in specified collection.
+         */
+        public EJDBQueryBuilder strOr(Collection<String> values) {
+            String[] strs = new String[values.size()];
+            values.toArray(strs);
+            return new Constraint("$stror", this).addOperation(strs);
+        }
+
+        /**
+         * Field existence matching {@link Constraint#exists(boolean = true)}
+         */
+        public EJDBQueryBuilder exists() {
+            return this.exists(true);
+        }
+
+        /**
+         * Field existence matching
+         */
+        public EJDBQueryBuilder exists(boolean exists) {
+            return new Constraint("$exists", this).addOperation(exists);
+        }
+
+        /**
+         * Case insensitive string matching
+         * <p/>
+         * Example:
+         * <code>query.field(field).icase().eq(value); // -> {field : {$icase : value}}</code>
+         * <code>query.field(field).icase().in(value1, value2); // -> {field : {$icase : {$in : [value1, value2]}}}</code>
+         */
+        public Constraint icase() {
+            return new Constraint("$icase", this);
+        }
+    }
+
+    /**
+     * Sorting rules for query results
+     */
+    public class OrderBy {
+        private BSONObject orderBy;
+
+        protected OrderBy(BSONObject orderBy) {
+            this.orderBy = orderBy;
+        }
+
+        /**
+         * Add ascending sorting order for field
+         *
+         * @param field BSON field path
+         */
+        public OrderBy asc(String field) {
+            return add(field, true);
+        }
+
+        /**
+         * Add descinding sorting order for field
+         *
+         * @param field BSON field path
+         */
+        public OrderBy desc(String field) {
+            return add(field, false);
+        }
+
+        /**
+         * Add sorting order for field
+         *
+         * @param field BSON field path
+         * @param asc   if <code>true</code> ascendong sorting order, otherwise - descinding
+         */
+        public OrderBy add(String field, boolean asc) {
+            orderBy.put(field, asc ? 1 : -1);
+            return this;
+        }
+
+        /**
+         * Clear all current sorting rules
+         */
+        public OrderBy clear() {
+            orderBy.clear();
+            return this;
+        }
+    }
+
+}
index f11bb8d..ee3a38e 100644 (file)
@@ -10,7 +10,7 @@ import java.util.NoSuchElementException;
  * @version $Id$
  */
 public class EJDBResultSet implements Iterable<BSONObject>, Iterator<BSONObject> {
-    private long rsPointer;
+    private transient long rsPointer;
 
     private int position;
 
@@ -38,17 +38,12 @@ public class EJDBResultSet implements Iterable<BSONObject>, Iterator<BSONObject>
         return position < this.length();
     }
 
-    public BSONObject next() {
+    public BSONObject next() throws EJDBException {
         if (!hasNext()) {
             throw new NoSuchElementException();
         }
 
-        try {
-            return get(position++);
-        } catch (EJDBException e) {
-            // TODO: ?
-            throw new RuntimeException(e);
-        }
+        return get(position++);
     }
 
     public void remove() {
index 7d0bd8b..ee7dfe2 100644 (file)
@@ -7,6 +7,7 @@ import org.ejdb.bson.types.ObjectId;
 import org.ejdb.driver.EJDB;
 import org.ejdb.driver.EJDBCollection;
 import org.ejdb.driver.EJDBQuery;
+import org.ejdb.driver.EJDBQueryBuilder;
 import org.ejdb.driver.EJDBResultSet;
 
 import java.io.ByteArrayOutputStream;
@@ -63,11 +64,11 @@ public class EJDBTest extends TestCase {
         BSONObject lobj = coll.load(oid);
         assertNotNull(lobj);
         assertEquals(obj, lobj);
-//        assertEquals(lobj.get("_id"), oid);
-//        assertEquals(obj.get("test"), lobj.get("test"));
-//        assertEquals(obj.get("test2"), lobj.get("test2"));
 
-        EJDBQuery query = coll.createQuery(new BSONObject());
+        EJDBQueryBuilder qb;
+
+        qb = new EJDBQueryBuilder();
+        EJDBQuery query = coll.createQuery(qb);
         EJDBResultSet rs = query.find();
         assertEquals(rs.length(), 1);
         for (BSONObject r : rs) {
@@ -78,12 +79,17 @@ public class EJDBTest extends TestCase {
 
         assertEquals(lobj, query.findOne());
 
-        EJDBQuery query2 = db.getCollection("test2").createQuery(new BSONObject());
+        qb = new EJDBQueryBuilder();
+        EJDBQuery query2 = db.getCollection("test2").createQuery(qb);
         assertNull(query2.findOne());
 
         assertEquals(query.count(), 1);
         assertEquals(query2.count(), 0);
 
+        qb = new EJDBQueryBuilder();
+        qb.field("test", "test")
+                .excludeField("test2");
+
         EJDBQuery query3 = coll.createQuery(new BSONObject("test", "test"), new BSONObject("$fields", new BSONObject("test2", 0)));
         assertEquals(query3.count(), 1);
 
@@ -133,34 +139,44 @@ public class EJDBTest extends TestCase {
         assertEquals(ss.get(0), obj12.getId());
         assertEquals(obj1, obj12);
 
-        EJDBQuery query = parrots.createQuery(new BSONObject());
+        EJDBQueryBuilder qb;
+
+        qb = new EJDBQueryBuilder();
+        EJDBQuery query = parrots.createQuery(qb);
         EJDBResultSet rs = query.find();
-        assertEquals(rs.length(), 2);
+        assertEquals(2, rs.length());
         rs.close();
 
-        query = parrots.createQuery(new BSONObject("name", Pattern.compile("(grenny|bounty)", Pattern.CASE_INSENSITIVE)),
-                                    new BSONObject("$orderby", new BSONObject("name", 1)));
+        qb = new EJDBQueryBuilder();
+        qb.field("name", Pattern.compile("(grenny|bounty)", Pattern.CASE_INSENSITIVE))
+                .orderBy().asc("name");
+
+        query = parrots.createQuery(qb);
 
         rs = query.find();
         assertEquals(rs.length(), 2);
         BSONObject robj1 = rs.next();
-        assertEquals(robj1.get("name"), "Bounty");
-        assertEquals(robj1.get("age"), 15);
+        assertEquals("Bounty", robj1.get("name"));
+        assertEquals(15, robj1.get("age"));
         rs.close();
 
-        query = parrots.createQuery(new BSONObject(),
-                                    new BSONObject[]{new BSONObject("name", "Grenny"), new BSONObject("name", "Bounty")},
-                                    new BSONObject("$orderby", new BSONObject("name", 1)));
+        qb = new EJDBQueryBuilder();
+        qb
+                .or().field("name", "Grenny")
+                .or().field("name", "Bounty");
+        qb.orderBy().asc("name");
+        query = parrots.createQuery(qb);
 
         rs = query.find();
-        assertEquals(rs.length(), 2);
+        assertEquals(2, rs.length());
         rs.close();
 
-        query = parrots.createQuery(new BSONObject(),
-                                    new BSONObject[]{new BSONObject("name", "Grenny")},
-                                    new BSONObject("$orderby", new BSONObject("name", 1)));
-
-        assertEquals(query.count(), 1);
+        qb = new EJDBQueryBuilder();
+        qb
+                .or().field("name", "Grenny");
+        qb.orderBy().asc("name");
+        query = parrots.createQuery(qb);
+        assertEquals(1, query.count());
     }
 
     public void testIndexes() throws Exception {
@@ -174,7 +190,7 @@ public class EJDBTest extends TestCase {
 
         ByteArrayOutputStream log;
 
-        EJDBQuery query = birds.createQuery(new BSONObject("name", "Molly"));
+        EJDBQuery query = birds.createQuery(new EJDBQueryBuilder().field("name", "Molly"));
 
         query.find(log = new ByteArrayOutputStream());
         assertTrue(log.toString().contains("RUN FULLSCAN"));