Skip to content
Browse files

Couchbase View implementation

* create DesignDocument instance
* execute Map step from View definition
* execute JS Reduce functions with possible grouping
* execute built-in Reduce functions: _sum, _count, _stats
* supported parameters:
  - descending
  - include_docs
  - key
  - keys
  - skip
  - limit
  - reduce
  - group
  - group_level
  - start_key
  - end_key
  - start_key_doc_id
  - end_key_doc_id
  - inclusive_end

Change-Id: If1d4f397c42a2e81d13bf23bd9023359edbde3e2
Reviewed-on: http://review.couchbase.org/15276
Tested-by: Sergey Avseyev <sergey.avseyev@gmail.com>
Reviewed-by: Jan Lehnardt <jan@couchbase.com>
Tested-by: Jan Lehnardt <jan@couchbase.com>
  • Loading branch information...
1 parent 6bdfb36 commit 229ab20b9a78dead2e090121aa3003ba87ddb703 @avsej avsej committed with Jan Lehnardt Apr 26, 2012
View
4 src/main/java/org/couchbase/mock/Bucket.java
@@ -31,6 +31,10 @@
*/
public abstract class Bucket {
+ public DataStore getDatastore() {
+ return datastore;
+ }
+
public enum BucketType {
MEMCACHE, COUCHBASE
}
View
8 src/main/java/org/couchbase/mock/memcached/DataStore.java
@@ -49,6 +49,14 @@ public VBucket getVBucket(short vbucket) {
return vBucketMap[vbucket];
}
+ public Map<String, Item>[] getData() {
+ Map<String, Item>[] res = new Map[vBucketMap.length];
+ for (int i = 0; i < vBucketMap.length; i++) {
+ res[i] = vBucketMap[i].getMap(vBucketMap[i].getOwner());
+ }
+ return res;
+ }
+
private Map<String, Item> getMap(MemcachedServer server, short vbucket) throws AccessControlException {
if (vbucket >= vBucketMap.length && server.getType() == BucketType.COUCHBASE) {
// Illegal vbucket.. just report as no access..
View
214 src/main/java/org/couchbase/mock/views/Configuration.java
@@ -0,0 +1,214 @@
+/**
+ * Copyright 2012 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.couchbase.mock.views;
+
+import java.util.List;
+
+/**
+ *
+ * @author Sergey Avseyev
+ */
+public class Configuration {
+
+ /* Include the full content of the documents in the return */
+ private boolean includeDocs = false;
+ /* Return the documents in descending by key order */
+ private boolean descending = false;
+ /* Stop returning records when the specified key is reached */
+ private String end_key;
+ /* Return records starting with the specified key */
+ private String start_key;
+ /* Stop returning records when the specified document ID is reached */
+ private String end_key_doc_id;
+ /* Return records starting with the specified document ID */
+ private String start_key_doc_id;
+ /* Group the results using the reduce function to a group or single row */
+ private boolean group = false;
+ /* Specify the group level to be used */
+ private int group_level;
+ /* Timeout before the view request is dropped */
+ private int connection_timeout;
+ /* Specifies whether the specified end key should be included in the result */
+ private boolean inclusive_end = true;
+ /* Return only documents that match the specified key */
+ private String key;
+ /* Return only documents that match the specified keys */
+ private List keys;
+ /* Limit the number of the returned documents to the specified number */
+ private Integer limit = null;
+ /* Sets the response in the event of an error.
+ * Possible values:
+ * * continue Continue to generate view information in the event of an
+ * error, including the error information in the view response
+ * stream.
+ *
+ * * stop Stop immediately when an error condition occurs. No further
+ * view information will be returned.
+ */
+ private String on_error = "continue";
+ /* Use the reduction function */
+ private boolean reduce = true;
+ /* Skip this number of records before starting to return the results */
+ private int skip = 0;
+ /* Allow the results from a stale view to be used.
+ * Possible values:
+ * * false Force a view update before returning data
+ * * ok Allow stale views
+ * * update_after Allow stale view, update view after it has been accessed
+ */
+ private String stale = "update_after";
+
+ public boolean includeDocs() {
+ return includeDocs;
+ }
+
+ public boolean isDescending() {
+ return descending;
+ }
+
+ public void setIncludeDocs(boolean includeDocs) {
+ this.includeDocs = includeDocs;
+ }
+
+ public void setDescending(boolean descending) {
+ this.descending = descending;
+ }
+
+ public String getEndKey() {
+ return end_key;
+ }
+
+ public void setEndKey(String endkey) {
+ this.end_key = endkey;
+ }
+
+ public String getStartKey() {
+ return start_key;
+ }
+
+ public void setStartKey(String startkey) {
+ this.start_key = startkey;
+ }
+
+ public boolean hasRange() {
+ return start_key != null || start_key_doc_id != null
+ || end_key != null || end_key_doc_id != null;
+ }
+
+ public String getEndKeyDocId() {
+ return end_key_doc_id;
+ }
+
+ public void setEndKeyDocId(String endkey_docid) {
+ this.end_key_doc_id = endkey_docid;
+ }
+
+ public String getStartKeyDocId() {
+ return start_key_doc_id;
+ }
+
+ public void setStartKeyDocId(String startkey_docid) {
+ this.start_key_doc_id = startkey_docid;
+ }
+
+ public boolean isGroup() {
+ return group;
+ }
+
+ public void setGroup(boolean group) {
+ this.group = group;
+ }
+
+ public int getGroupLevel() {
+ return group_level;
+ }
+
+ public void setGroupLevel(int group_level) {
+ this.group_level = group_level;
+ }
+
+ public int getConnectionTimeout() {
+ return connection_timeout;
+ }
+
+ public void setConnectionTimeout(int connection_timeout) {
+ this.connection_timeout = connection_timeout;
+ }
+
+ public boolean isInclusiveEnd() {
+ return inclusive_end;
+ }
+
+ public void setInclusiveEnd(boolean inclusive_end) {
+ this.inclusive_end = inclusive_end;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public Integer getLimit() {
+ return limit;
+ }
+
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ public String getOnError() {
+ return on_error;
+ }
+
+ public void setOnError(String on_error) {
+ this.on_error = on_error;
+ }
+
+ public boolean reduce() {
+ return reduce;
+ }
+
+ public void setReduce(boolean reduce) {
+ this.reduce = reduce;
+ }
+
+ public int getSkip() {
+ return skip;
+ }
+
+ public void setSkip(int skip) {
+ this.skip = skip;
+ }
+
+ public String getStale() {
+ return stale;
+ }
+
+ public void setStale(String stale) {
+ this.stale = stale;
+ }
+
+ public List getKeys() {
+ return keys;
+ }
+
+ public void setKeys(List keys) {
+ this.keys = keys;
+ }
+}
View
72 src/main/java/org/couchbase/mock/views/DesignDocument.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright 2012 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.couchbase.mock.views;
+
+import java.util.ArrayList;
+import java.util.Set;
+import javax.script.ScriptException;
+import net.sf.json.JSONException;
+import net.sf.json.JSONObject;
+import net.sf.json.JsonConfig;
+
+/**
+ *
+ * @author Sergey Avseyev
+ */
+public class DesignDocument {
+
+ private String body;
+ private String id;
+ private ArrayList<View> views;
+
+ public DesignDocument(String body) {
+ try {
+ this.body = body;
+ JsonConfig cfg = new JsonConfig();
+ cfg.clearJsonValueProcessors();
+ JSONObject json = (JSONObject) JSONObject.fromObject(body, cfg);
+ this.id = json.getString("_id");
+ this.views = new ArrayList<View>();
+ JSONObject viewsJson = (JSONObject) json.getJSONObject("views");
+ for (String viewName : (Set<String>) viewsJson.keySet()) {
+ JSONObject view = (JSONObject) viewsJson.getJSONObject(viewName);
+ String mapSrc = view.getString("map");
+ String reduceSrc = null;
+ try {
+ reduceSrc = view.getString("reduce");
+ } catch (JSONException _) {
+ // let it null
+ }
+ views.add(new View(viewName, mapSrc, reduceSrc));
+ }
+ } catch (ScriptException ex) {
+ } catch (JSONException ex) {
+ throw new IllegalArgumentException("Incomplete document body", ex);
+ }
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public ArrayList<View> getViews() {
+ return views;
+ }
+}
View
82 src/main/java/org/couchbase/mock/views/Mapper.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright 2012 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.couchbase.mock.views;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import net.sf.json.JSONException;
+import net.sf.json.JSONObject;
+import org.couchbase.mock.memcached.DataStore;
+import org.couchbase.mock.memcached.Item;
+
+/**
+ *
+ * @author Sergey Avseyev
+ */
+public class Mapper {
+
+ private final String body;
+ private final ScriptEngine engine;
+
+ public Mapper(ScriptEngine engine, String body) {
+ this.engine = engine;
+ this.body = body;
+ }
+
+ public ArrayList<HashMap> execute(DataStore store, Configuration config) {
+ ArrayList<HashMap> rows = new ArrayList<HashMap>();
+
+ for (Map<String, Item> map : store.getData()) {
+ for (Map.Entry<String, Item> entry : map.entrySet()) {
+ String jsonStr = new String(entry.getValue().getValue());
+ try { /* ensure that value is JSON document */
+ JSONObject json = JSONObject.fromObject(jsonStr);
+ json.put("_id", entry.getKey());
+ jsonStr = json.toString();
+ } catch (JSONException ex) {
+ continue;
+ }
+ try {
+ engine.eval("result = []");
+ engine.eval("(" + body + ")(" + jsonStr + ")");
+ ArrayList<ArrayList> result = (ArrayList<ArrayList>) View.fromNativeObject(engine.get("result"));
+ for (ArrayList row : result) {
+ HashMap document = new HashMap();
+ document.put("id", entry.getKey());
+ document.put("key", row.get(0));
+ document.put("value", row.get(1));
+ if (config.includeDocs()) {
+ Item item = entry.getValue();
+ JSONObject obj = JSONObject.fromObject(new String(item.getValue()));
+ obj.put("$flags", item.getFlags());
+ obj.put("$exp", item.getExptime());
+ document.put("doc", obj);
+ }
+ rows.add(document);
+ }
+ } catch (ScriptException ex) {
+ Logger.getLogger(View.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+ }
+ return rows;
+ }
+}
View
178 src/main/java/org/couchbase/mock/views/Reducer.java
@@ -0,0 +1,178 @@
+/**
+ * Copyright 2012 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.couchbase.mock.views;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Map.Entry;
+import javax.script.ScriptEngine;
+import javax.script.ScriptException;
+import net.sf.json.JSON;
+import net.sf.json.JSONArray;
+import net.sf.json.JSONSerializer;
+
+/**
+ *
+ * @author Sergey Avseyev
+ */
+public class Reducer {
+
+ private final String body;
+ private final ScriptEngine engine;
+
+ public Reducer(ScriptEngine engine, String body) {
+ this.engine = engine;
+ this.body = body;
+ }
+
+ public ArrayList execute(ArrayList rows, Configuration config) throws ScriptException {
+ if (body.startsWith("_count")) {
+ return executeCount(rows, config);
+ } else if (body.startsWith("_sum")) {
+ return executeSum(rows, config);
+ } else if (body.startsWith("_stats")) {
+ return executeStats(rows, config);
+ } else {
+ return executeJS(rows, config);
+ }
+ }
+
+ private class ReduceEntry {
+
+ public ArrayList keys = new ArrayList();
+ public ArrayList values = new ArrayList();
+
+ public void add(Object key, Object id, Object value) {
+ JSONArray json = new JSONArray();
+ json.add(key);
+ json.add(id);
+ keys.add(json);
+ values.add(value);
+ }
+ }
+
+ private HashMap<Object, ReduceEntry> toReduceParams(ArrayList rows, Configuration config) {
+ HashMap<Object, ReduceEntry> res = new HashMap<Object, ReduceEntry>();
+
+ for (HashMap row : (ArrayList<HashMap>) rows) {
+ Object key = null;
+ if (config.isGroup()) {
+ /* group by whole key */
+ key = row.get("key");
+ }
+ if (config.getGroupLevel() > 0) {
+ /* group by interval */
+ JSON json = JSONSerializer.toJSON(row.get("key"));
+ if (json instanceof JSONArray) {
+ ArrayList acc = new ArrayList();
+ for (int i = 0; i < config.getGroupLevel() && i < json.size(); ++i) {
+ acc.add(( (JSONArray) json ).get(i));
+ }
+ key = JSONArray.fromObject(acc).toString();
+ }
+ }
+ if (!res.containsKey(key)) {
+ res.put(key, new ReduceEntry());
+ }
+ res.get(key).add(row.get("key"), row.get("id"), row.get("value"));
+ }
+
+ return res;
+ }
+
+ private ArrayList executeJS(ArrayList rows, Configuration config) throws ScriptException {
+ ArrayList res = new ArrayList();
+ for (Entry<Object, ReduceEntry> entry : toReduceParams(rows, config).entrySet()) {
+ JSON group = JSONSerializer.toJSON(entry.getKey());
+ JSON params[] = new JSON[]{
+ JSONSerializer.toJSON(entry.getValue().keys),
+ JSONSerializer.toJSON(entry.getValue().values)
+ };
+ Object value = View.fromNativeObject(engine.eval("(" + body + ")(" + params[0].toString() + ", " + params[1].toString() + ", false)"));
+ HashMap reduced = new HashMap();
+ reduced.put("key", group);
+ reduced.put("value", value);
+ res.add(reduced);
+ }
+ return res;
+ }
+
+ private ArrayList executeCount(ArrayList rows, Configuration config) {
+ ArrayList res = new ArrayList();
+ for (Entry<Object, ReduceEntry> entry : toReduceParams(rows, config).entrySet()) {
+ JSON group = JSONSerializer.toJSON(entry.getKey());
+ HashMap reduced = new HashMap();
+ reduced.put("key", group);
+ reduced.put("value", entry.getValue().values.size());
+ res.add(reduced);
+ }
+ return res;
+ }
+
+ private ArrayList executeSum(ArrayList rows, Configuration config) {
+ ArrayList res = new ArrayList();
+ for (Entry<Object, ReduceEntry> entry : toReduceParams(rows, config).entrySet()) {
+ JSON group = JSONSerializer.toJSON(entry.getKey());
+ HashMap reduced = new HashMap();
+ reduced.put("key", group);
+ double sum = 0;
+ for (Object val : entry.getValue().values) {
+ sum += ( (Number) val ).doubleValue();
+ }
+ reduced.put("value", sum);
+ res.add(reduced);
+ }
+ return res;
+ }
+
+ private ArrayList executeStats(ArrayList rows, Configuration config) {
+ ArrayList res = new ArrayList();
+ for (Entry<Object, ReduceEntry> entry : toReduceParams(rows, config).entrySet()) {
+ JSON group = JSONSerializer.toJSON(entry.getKey());
+ HashMap reduced = new HashMap();
+ reduced.put("key", group);
+ ArrayList values = entry.getValue().values;
+ double sum = 0;
+ int count = values.size();
+ double min = 0;
+ double max = 0;
+ double sumsqr = 0;
+ if (count > 0) {
+ min = max = ( (Number) values.get(0) ).doubleValue();
+ }
+ for (Object val : entry.getValue().values) {
+ double d = ( (Number) val ).doubleValue();
+ sum += d;
+ sumsqr += Math.pow(d, 2);
+ if (d < min) {
+ min = d;
+ }
+ if (d > max) {
+ max = d;
+ }
+ }
+ HashMap stats = new HashMap();
+ stats.put("sum", sum);
+ stats.put("max", max);
+ stats.put("min", min);
+ stats.put("count", count);
+ stats.put("sumsqr", sumsqr);
+ reduced.put("value", stats);
+ res.add(reduced);
+ }
+ return res;
+ }
+}
View
365 src/main/java/org/couchbase/mock/views/View.java
@@ -0,0 +1,365 @@
+/**
+ * Copyright 2012 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.couchbase.mock.views;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import net.sf.json.JSONArray;
+import net.sf.json.JSONException;
+import net.sf.json.JSONObject;
+import net.sf.json.JSONSerializer;
+import net.sf.json.util.JSONUtils;
+import org.couchbase.mock.memcached.DataStore;
+import sun.org.mozilla.javascript.NativeArray;
+import sun.org.mozilla.javascript.Scriptable;
+import sun.org.mozilla.javascript.ScriptableObject;
+
+/**
+ *
+ * @author Sergey Avseyev
+ */
+public class View {
+
+ public class CompilationError extends Exception {
+ }
+
+ public static class RowComparator implements Comparator<HashMap> {
+
+ private Configuration config;
+
+ public RowComparator(Configuration config) {
+ this.config = config;
+ }
+
+ @Override
+ public int compare(HashMap o1, HashMap o2) {
+ Object key1 = View.parseJSON(o1.get("key"));
+ Object key2 = View.parseJSON(o2.get("key"));
+ Object id1 = View.parseJSON(o1.get("id"));
+ Object id2 = View.parseJSON(o2.get("id"));
+ int ret;
+
+ if (config.isDescending()) {
+ ret = jsonCompareTo(key2, key1);
+ if (ret == 0) {
+ ret = jsonCompareTo(id2, id1);
+ }
+ } else {
+ ret = jsonCompareTo(key1, key2);
+ if (ret == 0) {
+ ret = jsonCompareTo(id1, id2);
+ }
+ }
+ return ret;
+ }
+
+ public static int jsonCompareTo(Object a, Object b) {
+ int result;
+
+ if (a instanceof JSONObject) {
+ if (b instanceof JSONObject) {
+ JSONObject ao = (JSONObject) a;
+ JSONObject bo = (JSONObject) b;
+ Set as = ao.keySet();
+ Iterator ai = as.iterator();
+
+ while (true) {
+ if (!ai.hasNext()) {
+ if (bo.isEmpty()) {
+ return 0;
+ }
+ return -1;
+ }
+ if (bo.isEmpty()) {
+ return 1;
+ }
+ Object key = ai.next();
+ if (!bo.containsKey(key)) {
+ return 1;
+ }
+ result = jsonCompareTo(ao.get(key), bo.get(key));
+ bo.remove(key);
+ if (result != 0) {
+ return result;
+ }
+ }
+ } else {
+ return -1;
+ }
+ }
+ if (b instanceof JSONObject) {
+ return 1;
+ }
+ if (a instanceof JSONArray) {
+ if (b instanceof JSONArray) {
+ Iterator ai = ( (JSONArray) a ).iterator();
+ Iterator bi = ( (JSONArray) b ).iterator();
+
+ while (true) {
+ if (!ai.hasNext()) {
+ if (!bi.hasNext()) {
+ return 0;
+ }
+ return -1;
+ }
+ if (!bi.hasNext()) {
+ return 1;
+ }
+ result = jsonCompareTo(ai.next(), bi.next());
+ if (result != 0) {
+ return result;
+ }
+ }
+ } else {
+ return -1;
+ }
+ }
+ if (b instanceof JSONArray) {
+ return 1;
+ }
+ if (JSONUtils.isNumber(a) && JSONUtils.isNumber(b)) {
+ return (int) Math.floor(( (Number) a ).doubleValue() - ( (Number) b ).doubleValue());
+ }
+ /* in other cases values should be comparable */
+ Comparable ac = (Comparable) a;
+ Comparable bc = (Comparable) b;
+ return ac.compareTo(bc);
+ }
+ }
+ public static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssz");
+ private final String name;
+ private final String mapSource;
+ private final String reduceSource;
+ private final ScriptEngine jsEngine;
+ private final Mapper mapper;
+ private Reducer reducer;
+
+ public View(String name, String map) throws ScriptException {
+ this(name, map, null);
+ }
+
+ public View(String name, String map, String reduce) throws ScriptException {
+ this.name = name;
+ this.mapSource = map;
+ this.reduceSource = reduce;
+ jsEngine = new ScriptEngineManager().getEngineByName("javascript");
+ jsEngine.eval("emit = function(key, value) { result.push([key, value]) }");
+ jsEngine.eval("sum = function(values) { var sum = 0; for (var i = 0; i < values.length; ++i) { sum += values[i]; }; return sum; }");
+ this.mapper = new Mapper(jsEngine, this.mapSource);
+ if (this.reduceSource != null) {
+ this.reducer = new Reducer(jsEngine, this.reduceSource);
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getMapSource() {
+ return mapSource;
+ }
+
+ public String getReduceSource() {
+ return reduceSource;
+ }
+
+ public HashMap execute(DataStore store) {
+ return execute(store, null);
+ }
+
+ public HashMap execute(DataStore store, Configuration config) {
+ ArrayList<HashMap> rows;
+
+ if (config == null) {
+ config = new Configuration();
+ }
+
+ rows = mapper.execute(store, config);
+ if (config.reduce() && reducer != null) {
+ try {
+ rows = reducer.execute(rows, config);
+ } catch (ScriptException ex) {
+ Logger.getLogger(View.class.getName()).log(Level.SEVERE, null, ex);
+ }
+ }
+ Collections.sort(rows, new RowComparator(config));
+
+ int totalRows = rows.size();
+ int offset = 0;
+ int end = rows.size();
+
+ /* ranging: start_key, end_key, start_key_docid, end_key_docid */
+ if (config.hasRange()) {
+ Object startKey = parseJSON(config.getStartKey());
+ Object startKeyDocId = parseJSON(config.getStartKeyDocId());
+ Object endKey = parseJSON(config.getEndKey());
+ Object endKeyDocId = parseJSON(config.getEndKeyDocId());
+ if (config.isDescending()) {
+ Object tmp;
+ tmp = startKey;
+ startKey = endKey;
+ endKey = tmp;
+ tmp = startKeyDocId;
+ startKeyDocId = endKeyDocId;
+ endKeyDocId = tmp;
+ }
+ for (int i = 0; i < rows.size(); ++i) {
+ HashMap row = rows.get(i);
+ Object key = row.get("key");
+ Object id = row.get("id");
+ if (key.equals(startKey) && offset == 0
+ && ( startKeyDocId == null
+ || ( startKeyDocId != null && id.equals(startKeyDocId) ) )) {
+ offset = i;
+ }
+ if (key.equals(endKey)
+ && ( endKeyDocId == null
+ || ( endKeyDocId != null && id.equals(endKeyDocId) ) )) {
+ if (!config.isInclusiveEnd()) {
+ end = i - 1;
+ break;
+ } else {
+ end = i;
+ }
+ }
+ }
+ while (end < rows.size() - 1) {
+ rows.remove(rows.size() - 1);
+ }
+ }
+
+ /* pagination: skip, limit */
+ offset += config.getSkip();
+ if (offset > rows.size()) {
+ offset = rows.size();
+ }
+ for (int i = 0; i < offset; ++i) {
+ rows.remove(0);
+ }
+ if (config.getLimit() != null) {
+ while (rows.size() > config.getLimit()) {
+ rows.remove(rows.size() - 1);
+ }
+ }
+
+ /* filtering: key, keys */
+ ArrayList filter = new ArrayList();
+ if (config.getKey() != null) {
+ filter.add(config.getKey());
+ }
+ if (config.getKeys() != null) {
+ filter.addAll(config.getKeys());
+ }
+ if (!filter.isEmpty()) {
+ for (int i = 0; i < filter.size(); ++i) {
+ try {
+ filter.set(i, JSONSerializer.toJSON(filter.get(i)));
+ } catch (JSONException ex) {
+ }
+ }
+ ArrayList<HashMap> filtered = new ArrayList<HashMap>();
+ for (HashMap row : rows) {
+ if (filter.contains(row.get("key"))) {
+ filtered.add(row);
+ }
+ }
+ rows = filtered;
+ }
+
+ HashMap response = new HashMap();
+ response.put("total_rows", totalRows);
+ response.put("offset", offset);
+ response.put("rows", rows);
+ return response;
+ }
+
+ public static Object fromNativeObject(Object object) {
+ if (object instanceof NativeArray) {
+ NativeArray array = (NativeArray) object;
+ int length = (int) array.getLength();
+ ArrayList json = new ArrayList();
+ for (int i = 0; i < length; i++) {
+ json.add(fromNativeObject(ScriptableObject.getProperty(array, i)));
+ }
+ return json;
+ } else if (object instanceof ScriptableObject) {
+ ScriptableObject scriptable = (ScriptableObject) object;
+ HashMap json = new HashMap();
+
+ Object[] ids = scriptable.getAllIds();
+ for (Object id : ids) {
+ String key = id.toString();
+ Object property = ScriptableObject.getProperty(scriptable, key);
+ Object value = null;
+ if (property instanceof Scriptable && ( (Scriptable) property ).getClassName().equals("Date")) {
+ // Convert NativeDate to Date
+
+ // (The NativeDate class is private in Rhino, but we can access
+ // it like a regular object.)
+ Object time = ScriptableObject.callMethod(scriptable, "getTime", null);
+ if (time instanceof Number) {
+ value = DATE_FORMAT.format(new Date(( (Number) time ).longValue()));
+ }
+ } else {
+ value = fromNativeObject(property);
+ }
+ json.put(key, value);
+ }
+ return json;
+ } else if (object instanceof Integer) {
+ return ( (Number) object ).longValue();
+ } else if (object instanceof Double || object instanceof Float) {
+ Double d = ( (Number) object ).doubleValue();
+ if (d % 1 == 0.0) {
+ return d.longValue();
+ }
+ return object;
+ } else {
+ return object;
+ }
+ }
+
+ public static Object parseJSON(Object object) {
+ if (object == null || JSONUtils.isNumber(object) || JSONUtils.isBoolean(object)) {
+ return object;
+ } else {
+ try {
+ return JSONSerializer.toJSON(object);
+ } catch (JSONException e1) {
+ try {
+ return Long.parseLong((String) object);
+ } catch (NumberFormatException e2) {
+ try {
+ return Double.parseDouble((String) object);
+ } catch (NumberFormatException e3) {
+ return object;
+ }
+ }
+ }
+ }
+ }
+}
View
428 src/test/java/org/couchbase/mock/views/ViewTest.java
@@ -0,0 +1,428 @@
+/**
+ * Copyright 2012 Couchbase, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.couchbase.mock.views;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import junit.framework.TestCase;
+import net.sf.json.JSON;
+import net.sf.json.JSONObject;
+import org.couchbase.mock.CouchbaseBucket;
+import org.couchbase.mock.memcached.DataStore;
+import org.couchbase.mock.memcached.Item;
+import org.couchbase.mock.memcached.MemcachedServer;
+
+import net.sf.json.JSONSerializer;
+
+/**
+ *
+ * @author Sergey Avseyev
+ */
+public class ViewTest extends TestCase {
+
+ public void testParser() {
+ String body = "{"
+ + " \"_id\": \"_design/blog\","
+ + " \"language\": \"javascript\","
+ + " \"views\": {"
+ + " \"recent_posts\": {"
+ + " \"map\": \"function(doc){ if(doc.date && doc.title){emit(doc.date, doc.title);} }\""
+ + " }"
+ + " }"
+ + "}";
+ DesignDocument ddoc = new DesignDocument(body);
+ assertEquals("_design/blog", ddoc.getId());
+ assertEquals(body, ddoc.getBody());
+ ArrayList<View> views = ddoc.getViews();
+ assertNotNull(views);
+ assertEquals(1, views.size());
+ View recentPosts = views.get(0);
+ assertEquals("recent_posts", recentPosts.getName());
+ assertEquals("function(doc){ if(doc.date && doc.title){emit(doc.date, doc.title);} }", recentPosts.getMapSource());
+ assertEquals(null, recentPosts.getReduceSource());
+ }
+
+ public void testDecodingPrimitives() throws ScriptException {
+ ScriptEngine jsEngine = new ScriptEngineManager().getEngineByName("javascript");
+ String wrapper = "(function(){ return XXX; })()";
+
+ assertEquals(Long.class, View.fromNativeObject(jsEngine.eval(wrapper.replace("XXX", "123"))).getClass());
+ assertEquals(Double.class, View.fromNativeObject(jsEngine.eval(wrapper.replace("XXX", "123.567"))).getClass());
+ assertEquals(ArrayList.class, View.fromNativeObject(jsEngine.eval(wrapper.replace("XXX", "[123, 567]"))).getClass());
+ assertEquals(HashMap.class, View.fromNativeObject(jsEngine.eval(wrapper.replace("XXX", "{'123': 567}"))).getClass());
+ }
+
+ public void testDecodingCompound() throws ScriptException {
+ ScriptEngine jsEngine = new ScriptEngineManager().getEngineByName("javascript");
+ String wrapper = "(function(){ return XXX; })()";
+ String compound = "[{'foo': [1.3, 'hello']}, {'bar': {'who': 'world'}}]";
+ Object value = View.fromNativeObject(jsEngine.eval(wrapper.replace("XXX", compound)));
+
+ assertEquals(ArrayList.class, value.getClass());
+ ArrayList array = (ArrayList) value;
+ assertEquals(2, array.size());
+
+ assertEquals(HashMap.class, array.get(0).getClass());
+ HashMap first = (HashMap) array.get(0);
+ assertEquals(1, first.size());
+ assertTrue(first.containsKey("foo"));
+ assertEquals(ArrayList.class, first.get("foo").getClass());
+ ArrayList foo = (ArrayList) first.get("foo");
+ assertEquals(1.3, foo.get(0));
+ assertEquals("hello", foo.get(1));
+
+ assertEquals(HashMap.class, array.get(1).getClass());
+ HashMap second = (HashMap) array.get(1);
+ assertEquals(1, second.size());
+ assertTrue(second.containsKey("bar"));
+ assertEquals(HashMap.class, second.get("bar").getClass());
+ HashMap bar = (HashMap) second.get("bar");
+ assertTrue(bar.containsKey("who"));
+ assertEquals("world", bar.get("who"));
+ }
+
+ private DataStore seedDocuments(int num) throws IOException {
+ /* single server and 16 vbuckets */
+ CouchbaseBucket bucket = new CouchbaseBucket("test", "127.0.0.1", 9000, 1, 0, 16);
+ MemcachedServer server = bucket.getServers()[0];
+ DataStore store = bucket.getDatastore();
+ for (int i = 0; i < num; i++) {
+ String key = String.format("key-%03d", i);
+ byte[] val = ( "{\"val\": " + Integer.toString(i) + "}" ).getBytes();
+ store.add(server, (short) ( i % 16 ), new Item(key, 12345678, 0, val, 0));
+ }
+ return store;
+ }
+
+ public void testMapperTrivial() throws ScriptException, IOException {
+ DataStore store = seedDocuments(40);
+ View view = new View("all", "function(doc){emit(doc._id, null)}", null);
+ HashMap results = view.execute(store); /* execute with default config */
+
+ Integer total_rows = (Integer) results.get("total_rows");
+ assertEquals(40, (int) total_rows);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(40, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("key-000", (String) firstRow.get("key"));
+ }
+
+ public void testSkipLimit() throws ScriptException, IOException {
+ DataStore store = seedDocuments(40);
+ View view = new View("all", "function(doc){emit(doc._id, null)}", null);
+ Configuration config = new Configuration();
+ config.setSkip(10);
+ config.setLimit(5);
+ HashMap results = view.execute(store, config);
+
+ Integer total_rows = (Integer) results.get("total_rows");
+ assertEquals(40, (int) total_rows);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(5, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("key-010", (String) firstRow.get("key"));
+ }
+
+ public void testRanging() throws ScriptException, IOException {
+ DataStore store = seedDocuments(40);
+ View view = new View("all", "function(doc){emit(doc.val); emit(doc.val+1);}", null);
+
+ Configuration config = new Configuration();
+ config.setStartKey("1");
+ config.setEndKey("3");
+ /* filter results: 1, 1, 2, 2, 3, 3 */
+ HashMap results = view.execute(store, config);
+ Integer total_rows = (Integer) results.get("total_rows");
+ assertEquals(80, (int) total_rows);
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(6, rows.size());
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("1", firstRow.get("key").toString());
+ assertEquals("key-000", (String) firstRow.get("id"));
+
+ /* filter results: 3, 3, 2, 2, 1, 1 */
+ config.setDescending(true);
+ results = view.execute(store, config);
+ total_rows = (Integer) results.get("total_rows");
+ assertEquals(80, (int) total_rows);
+ rows = (ArrayList) results.get("rows");
+ assertEquals(6, rows.size());
+ firstRow = (HashMap) rows.get(0);
+ assertEquals("3", firstRow.get("key").toString());
+ assertEquals("key-003", (String) firstRow.get("id"));
+ }
+
+ public void testRangingExclusiveEnd() throws ScriptException, IOException {
+ DataStore store = seedDocuments(40);
+ View view = new View("all", "function(doc){emit(doc.val); emit(doc.val+1);}", null);
+
+ Configuration config = new Configuration();
+ config.setStartKey("1");
+ config.setEndKey("3");
+ config.setInclusiveEnd(false);
+ /* filter results: 1, 1, 2, 2 */
+ HashMap results = view.execute(store, config);
+ Integer total_rows = (Integer) results.get("total_rows");
+ assertEquals(80, (int) total_rows);
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(4, rows.size());
+ }
+
+ public void testMapperDescending() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all", "function(doc){emit(doc._id, null)}", null);
+ Configuration config = new Configuration();
+ config.setDescending(true);
+ HashMap results = view.execute(store, config);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(9, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("key-008", (String) firstRow.get("key"));
+ }
+
+ public void testMapperIncludeDocs() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all", "function(doc){emit(doc._id, null)}", null);
+ Configuration config = new Configuration();
+ config.setIncludeDocs(true);
+ HashMap results = view.execute(store, config);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(9, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("key-000", (String) firstRow.get("key"));
+ assertEquals("{\"val\":0,\"$flags\":12345678,\"$exp\":0}", firstRow.get("doc").toString());
+ }
+
+ public void testMapperEmittingCustomKey() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all", "function(doc){emit(doc._id.toUpperCase(), (doc.val + 1).toString())}", null);
+ HashMap results = view.execute(store);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(9, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("key-000", (String) firstRow.get("id"));
+ assertEquals("KEY-000", (String) firstRow.get("key"));
+ assertEquals("1", (String) firstRow.get("value"));
+ assertEquals(null, firstRow.get("doc"));
+ }
+
+ public void testReduceCount() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all",
+ "function(doc){emit(doc._id)}",
+ "function(keys, values, rereduce){ return values.length; }");
+ HashMap results = view.execute(store);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("null", firstRow.get("key").toString());
+ assertEquals(9, ( (Number) firstRow.get("value") ).intValue());
+ }
+
+ public void testReduceCountBuiltin() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all", "function(doc){emit(doc._id)}", "_count");
+ HashMap results = view.execute(store);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("null", firstRow.get("key").toString());
+ assertEquals(9, ( (Number) firstRow.get("value") ).intValue());
+ }
+
+ public void testReduceSum() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all",
+ "function(doc){emit(doc._id, doc.val)}",
+ "function(keys, values, rereduce){ return sum(values); }");
+ HashMap results = view.execute(store);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("null", firstRow.get("key").toString());
+ assertEquals(36, ( (Number) firstRow.get("value") ).intValue());
+ }
+
+ public void testReduceSumBuiltin() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all", "function(doc){emit(doc._id, doc.val)}", "_sum");
+ HashMap results = view.execute(store);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("null", firstRow.get("key").toString());
+ assertEquals(36, ( (Number) firstRow.get("value") ).intValue());
+ }
+
+ public void testReduceStatsBuiltin() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all", "function(doc){emit(doc._id, doc.val)}", "_stats");
+ HashMap results = view.execute(store);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("null", firstRow.get("key").toString());
+ HashMap stats = (HashMap) firstRow.get("value");
+ assertEquals(36, ( (Number) stats.get("sum") ).intValue());
+ assertEquals(204, ( (Number) stats.get("sumsqr") ).intValue());
+ assertEquals(9, ( (Number) stats.get("count") ).intValue());
+ assertEquals(0, ( (Number) stats.get("min") ).intValue());
+ assertEquals(8, ( (Number) stats.get("max") ).intValue());
+ }
+
+ public void testJsonOrdering() {
+ JSON a, b;
+
+ a = JSONSerializer.toJSON("[1, '10']");
+ b = JSONSerializer.toJSON("[1, '9']");
+ assertTrue(View.RowComparator.jsonCompareTo(a, b) < 0);
+
+ a = JSONSerializer.toJSON("[1, 10]");
+ b = JSONSerializer.toJSON("[1, 9]");
+ assertTrue(View.RowComparator.jsonCompareTo(a, b) > 0);
+
+ a = JSONSerializer.toJSON("{'foo': 1, 'bar': 2}");
+ b = JSONSerializer.toJSON("{'bar': 2, 'foo': 1}");
+ assertTrue(View.RowComparator.jsonCompareTo(a, b) == 0);
+
+ a = JSONSerializer.toJSON("[1, {'foo': 1, 'bar': 2}]");
+ b = JSONSerializer.toJSON("[1, {'bar': 2, 'foo': 1}]");
+ assertTrue(View.RowComparator.jsonCompareTo(a, b) == 0);
+ }
+
+ public void testReduceGroupCount() throws IOException, ScriptException {
+ DataStore store = seedDocuments(9);
+ View view = new View("all",
+ "function(doc){if (doc.val % 2 == 0) { emit([\"odd\", doc._id])} else { emit([\"even\", doc._id]) } }",
+ "function(keys, values, rereduce){ return values.length; }");
+
+ HashMap results = view.execute(store);
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("null", firstRow.get("key").toString());
+ assertEquals(9, ( (Number) firstRow.get("value") ).intValue());
+
+ Configuration config = new Configuration();
+ config.setGroup(true);
+ results = view.execute(store, config);
+ rows = (ArrayList) results.get("rows");
+ assertEquals(9, rows.size());
+ firstRow = (HashMap) rows.get(0);
+ assertEquals("[\"even\",\"key-001\"]", firstRow.get("key").toString());
+ assertEquals(1, ( (Number) firstRow.get("value") ).intValue());
+
+ config = new Configuration();
+ config.setGroup(true);
+ config.setGroupLevel(1);
+ results = view.execute(store, config);
+ rows = (ArrayList) results.get("rows");
+ assertEquals(2, rows.size());
+ firstRow = (HashMap) rows.get(0);
+ assertEquals("[\"even\"]", firstRow.get("key").toString());
+ assertEquals(4, ( (Number) firstRow.get("value") ).intValue());
+
+ config = new Configuration();
+ config.setGroup(true);
+ config.setGroupLevel(0);
+ results = view.execute(store);
+ rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+ firstRow = (HashMap) rows.get(0);
+ assertEquals("null", firstRow.get("key").toString());
+ assertEquals(9, ( (Number) firstRow.get("value") ).intValue());
+
+ config = new Configuration();
+ config.setGroup(true);
+ config.setGroupLevel(10);
+ results = view.execute(store, config);
+ rows = (ArrayList) results.get("rows");
+ assertEquals(9, rows.size());
+ firstRow = (HashMap) rows.get(0);
+ assertEquals("[\"even\",\"key-001\"]", firstRow.get("key").toString());
+ assertEquals(1, ( (Number) firstRow.get("value") ).intValue());
+ }
+
+ public void testItAllowsToTurnOffReduce() throws ScriptException, IOException {
+ DataStore store = seedDocuments(40);
+ View view = new View("all", "function(doc){emit(doc._id, null)}", "_count");
+ Configuration config = new Configuration();
+ config.setReduce(false);
+ HashMap results = view.execute(store, config);
+
+ Integer total_rows = (Integer) results.get("total_rows");
+ assertEquals(40, (int) total_rows);
+
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(40, rows.size());
+
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("key-000", (String) firstRow.get("key"));
+ }
+
+ public void testFilters() throws ScriptException, IOException {
+ DataStore store = seedDocuments(40);
+ View view = new View("all", "function(doc){emit({'id': doc._id}, null)}");
+
+ Configuration config = new Configuration();
+ config.setKey("{ 'id' : 'key-001' }");
+ HashMap results = view.execute(store, config);
+ Integer total_rows = (Integer) results.get("total_rows");
+ assertEquals(40, (int) total_rows);
+ ArrayList rows = (ArrayList) results.get("rows");
+ assertEquals(1, rows.size());
+ HashMap firstRow = (HashMap) rows.get(0);
+ assertEquals("{\"id\":\"key-001\"}", JSONObject.fromObject(firstRow.get("key")).toString());
+
+ config = new Configuration();
+ ArrayList keys = new ArrayList();
+ keys.add("{'id': 'key-006'}");
+ keys.add("{'id': 'key-008'}");
+ config.setKeys(keys);
+ results = view.execute(store, config);
+ total_rows = (Integer) results.get("total_rows");
+ assertEquals(40, (int) total_rows);
+ rows = (ArrayList) results.get("rows");
+ assertEquals(2, rows.size());
+ firstRow = (HashMap) rows.get(0);
+ assertEquals("{\"id\":\"key-006\"}", JSONObject.fromObject(firstRow.get("key")).toString());
+ }
+
+}

0 comments on commit 229ab20

Please sign in to comment.
Something went wrong with that request. Please try again.