/**
* 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 org.couchbase.mock.JsonUtils;
import org.couchbase.mock.memcached.Item;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.mozilla.javascript.*;
import javax.script.ScriptException;
/**
* This represents a compiled Couchbase View that is part of the bucket. A view
* consists of a {@link org.couchbase.mock.views.DesignDocument} consisting of a
* {@code map} function and optionally a {@code reduce} function.
*
* @author Sergey Avseyev
* @author Mark Nunberg
*/
public class View {
private final String name;
private final String mapSource;
private final String reduceSource;
private final Indexer indexer;
private final Reducer reducer;
private final JavascriptRun jsRun;
public View(String name, String map) throws ScriptException {
this(name, map, null);
}
/**
* Create a new view
* @param name The name of the view
* @param map The JavaScript map function as a String
* @param reduce The JavaScript reduce function, as a String
* @throws org.mozilla.javascript.EcmaError if the map or reduce functions could not be parsed
*/
public View(@NotNull String name, @NotNull String map, @Nullable String reduce) throws ScriptException {
this.name = name;
this.mapSource = map;
this.reduceSource = reduce;
this.jsRun = new JavascriptRun();
this.indexer = Indexer.create(map);
if (reduce != null) {
this.reducer = Reducer.create(reduce);
} else {
this.reducer = null;
}
}
/**
* Gets the name of the view
* @return The name of the view
*/
public String getName() {
return name;
}
/**
* Gets the JavaScript source code for the map function
* @return The map source code
*/
public String getMapSource() {
return mapSource;
}
/**
* Gets the JavaScript source code for the reduce function
* @return The reduce source code
*/
public String getReduceSource() {
return reduceSource;
}
public QueryResult execute(Iterable<Item> items) throws QueryExecutionException {
return execute(items, null);
}
/**
* Executes the view query with the given parameters
* @param items The items in the bucket which should be processed via the view
* @param config The configuration for the view, acting as a filter on the items processed
* @return A results object which may be inspected
* @throws QueryExecutionException If there was an error while processing the options
* @see {@link #executeRaw(Iterable, Configuration)}
*/
public QueryResult execute(Iterable<Item> items, Configuration config) throws QueryExecutionException {
return new QueryResult(JsonUtils.decodeAsMap(executeRaw(items, config)));
}
/**
* Executes the view query with the given parameters.
* @param items The items to be indexed
* @param config The configuration to use for filters
* @return A string suitable for returning to a Couchbase client
* @throws QueryExecutionException
*/
public String executeRaw(Iterable<Item> items, Configuration config) throws QueryExecutionException {
if (config == null) {
config = new Configuration();
}
Context cx = Context.enter();
Scriptable scope = cx.initStandardObjects();
NativeObject configObject = config.toNativeObject();
Scriptable redFunc = null;
if (reducer != null) {
redFunc = reducer.getFunction();
}
try {
// long indexStart = System.currentTimeMillis();
indexer.run(items, cx);
// long indexEnd = System.currentTimeMillis();
// System.err.printf("Indexing took %d ms%n", indexEnd-indexStart);
Scriptable indexResults = indexer.getLastResults();
Scriptable resultObject;
try {
// long filterStart = System.currentTimeMillis();
resultObject = jsRun.execute(configObject, indexResults, redFunc, cx);
// long filterEnd = System.currentTimeMillis();
// System.err.printf("Filtering took %d ms%n", filterEnd-filterStart);
} catch (JavaScriptException ex) {
Object thrownObject = ex.getValue();
String jsonException;
try {
jsonException = (String) NativeJSON.stringify(cx, scope, thrownObject, null, null);
throw new QueryExecutionException(jsonException);
} catch (EcmaError ex2) {
throw new QueryExecutionException(ex2.getErrorMessage());
}
} catch (EcmaError parseErr) {
throw new QueryExecutionException(parseErr.getErrorMessage());
}
NativeArray rows = (NativeArray) resultObject.get("rows", resultObject);
resultObject.delete("rows");
StringBuilder sb = new StringBuilder();
sb.append("{");
for (Object id : ((NativeObject)resultObject).getAllIds()) {
if (! (id instanceof String)) {
throw new RuntimeException("ARGH: " + id);
}
sb.append('"').append(id).append("\":");
sb.append((String)NativeJSON.stringify(cx, scope, resultObject.get((String)id, resultObject), null, null));
sb.append(",");
}
sb.append("\"rows\":[\n");
for (int i = 0; i < rows.size(); i++) {
Object o = rows.get(i, rows);
sb.append((String)NativeJSON.stringify(cx, scope, o, null, null));
if (i < rows.size()-1) {
sb.append(",");
}
sb.append("\n");
}
sb.append("]\n");
sb.append("}\n");
return sb.toString();
} finally {
Context.exit();
}
}
}