org.lightcouch.View.java Source code

Java tutorial

Introduction

Here is the source code for org.lightcouch.View.java

Source

/*
 * Copyright (C) 2011 Ahmed Yehia (ahmed.yehia.m@gmail.com)
 * 
 * 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.lightcouch;

import static java.lang.String.format;
import static org.lightcouch.CouchDbUtil.JsonToObject;
import static org.lightcouch.CouchDbUtil.assertNotEmpty;
import static org.lightcouch.CouchDbUtil.close;
import static org.lightcouch.CouchDbUtil.getElementAsInt;
import static org.lightcouch.CouchDbUtil.getElementAsLong;
import static org.lightcouch.CouchDbUtil.listResources;
import static org.lightcouch.CouchDbUtil.readFile;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpResponse;
import org.lightcouch.DesignDocument.MapReduce;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

/**
 * <p>
 * This class allows construction and sending of View query requests. The API
 * supports view queries for various data type results, and for pagination.
 * 
 * <h3>Usage Example:</h3>
 * 
 * <pre>
 * {
 *    &#064;code
 *    List&lt;Foo&gt; list = dbClient.view(&quot;example/foo&quot;).includeDocs(true).startKey(&quot;start-key&quot;).endKey(&quot;end-key&quot;).limit(10)
 *          .query(Foo.class);
 * 
 *    int count = dbClient.view(&quot;example/by_tag&quot;).key(&quot;couchdb&quot;).queryForInt(); // query for scalar values
 * 
 *    // query for view entries
 *    View view = dbClient.view(&quot;example/by_date&quot;).key(2011, 10, 15) // complex key example
 *          .reduce(false).includeDocs(true);
 *    ViewResult&lt;int[], String, Foo&gt; result = view.queryView(int[].class, String.class, Foo.class);
 * 
 *    // pagination
 *    Page&lt;Foo&gt; page = dbClient.view(&quot;example/foo&quot;).queryPage(15, param, Foo.class);
 *    // page.get*Param() contains the param to query subsequent pages, {@code null} param queries the first page
 * }
 * </pre>
 * 
 * @author Ahmed Yehia
 */
public class View {
    private static final Log log = LogFactory.getLog(View.class);

    // paging param fields
    private static final String START_KEY = "s_k";
    private static final String START_KEY_DOC_ID = "s_k_d_i";
    private static final String CURRENT_START_KEY = "c_k";
    private static final String CURRENT_START_KEY_DOC_ID = "c_k_d_i";
    private static final String CURRENT_KEYS = "c";
    private static final String ACTION = "a";
    private static final String NEXT = "n";
    private static final String PREVIOUS = "p";

    // temp views
    private static final String TEMP_VIEWS_DIR = "temp-views";
    private static final String MAP_JS = "map.js";
    private static final String REDUCE_JS = "reduce.js";

    // view fields
    private String key;
    private String startKey;
    private String startKeyDocId;
    private String endKey;
    private String endKeyDocId;
    private Integer limit;
    private String stale;
    private Boolean descending;
    private Integer skip;
    private Boolean group;
    private Integer groupLevel;
    private Boolean reduce;
    private Boolean includeDocs;
    private Boolean inclusiveEnd;
    private Boolean updateSeq;

    private CouchDbClient dbc;
    private Gson gson;
    private URIBuilder uriBuilder;

    private String allDocsKeys; // bulk docs
    private MapReduce tempView; // temp view

    View(CouchDbClient dbc, String viewId) {
        assertNotEmpty(viewId, "View id");
        this.dbc = dbc;
        this.gson = dbc.getGson();

        String view = viewId;
        if (viewId.contains("/")) {
            String[] v = viewId.split("/");
            view = String.format("_design/%s/_view/%s", v[0], v[1]);
        }
        this.uriBuilder = URIBuilder.builder(dbc.getDBUri()).path(view);
    }

    // Query options

    /**
     * Queries a view as an {@link InputStream}
     * <p>
     * The stream should be properly closed after usage, as to avoid connection
     * leaks.
     * 
     * @return The result as an {@link InputStream}.
     */
    public InputStream queryForStream() {
        URI uri = uriBuilder.build();
        if (allDocsKeys != null) { // bulk docs
            HttpResponse response = dbc.post(uri, allDocsKeys);
            return dbc.getStream(response);
        }
        if (tempView != null) { // temp view
            return dbc.getStream(dbc.post(uri, gson.toJson(tempView)));
        }

        return dbc.get(uri);
    }

    /**
     * Queries a view.
     * 
     * @param <T>
     *            Object type T
     * @param classOfT
     *            The class of type T
     * @return The result of the view query as a {@code List<T> }
     */
    public <T> List<T> query(Class<T> classOfT) {
        InputStream instream = null;
        try {
            Reader reader = new InputStreamReader(instream = queryForStream(), StandardCharsets.UTF_8);
            JsonArray jsonArray = new JsonParser().parse(reader).getAsJsonObject().getAsJsonArray("rows");
            List<T> list = new ArrayList<T>();
            for (JsonElement jsonElem : jsonArray) {
                JsonElement elem = jsonElem.getAsJsonObject();
                if (Boolean.TRUE.equals(this.includeDocs)) {
                    elem = jsonElem.getAsJsonObject().get("doc");
                }
                T t = this.gson.fromJson(elem, classOfT);
                list.add(t);
            }
            return list;
        } finally {
            close(instream);
        }
    }

    /**
     * Queries a view.
     * 
     * @param <K>
     *            Object type K (key)
     * @param <V>
     *            Object type V (value)
     * @param classOfK
     *            The class of type K.
     * @param classOfV
     *            The class of type V.
     * @param classOfT
     *            The class of type T.
     * @return The View result entries.
     */
    public <K, V, T> ViewResult<K, V, T> queryView(Class<K> classOfK, Class<V> classOfV, Class<T> classOfT) {
        InputStream instream = null;
        try {
            Reader reader = new InputStreamReader(instream = queryForStream(), StandardCharsets.UTF_8);
            JsonObject json = new JsonParser().parse(reader).getAsJsonObject();
            ViewResult<K, V, T> vr = new ViewResult<K, V, T>();
            vr.setTotalRows(getElementAsLong(json, "total_rows"));
            vr.setOffset(getElementAsInt(json, "offset"));
            vr.setUpdateSeq(getElementAsLong(json, "update_seq"));
            JsonArray jsonArray = json.getAsJsonArray("rows");
            if (jsonArray.size() == 0) { // validate available rows
                throw new NoDocumentException("No result was returned by this view query.");
            }
            for (JsonElement e : jsonArray) {
                ViewResult<K, V, T>.Rows row = vr.new Rows();
                row.setId(JsonToObject(gson, e, "id", String.class));
                row.setKey(JsonToObject(gson, e, "key", classOfK));

                // cplutte added, allow String.class as classparam to return objects as jsonString
                if (classOfT.isAssignableFrom(String.class)) {
                    JsonObject va = e.getAsJsonObject().getAsJsonObject("value");
                    row.setValue((V) va.toString());
                    if (Boolean.TRUE.equals(this.includeDocs)) {
                        va = e.getAsJsonObject().getAsJsonObject("doc");
                        row.setDoc((T) va.toString());
                    }
                } else {
                    row.setValue(JsonToObject(gson, e, "value", classOfV));
                    if (Boolean.TRUE.equals(this.includeDocs)) {
                        row.setDoc(JsonToObject(gson, e, "doc", classOfT));
                    }
                }
                vr.getRows().add(row);
            }
            return vr;
        } finally {
            close(instream);
        }
    }

    /**
     * @return The result of the view as String.
     */
    public String queryForString() {
        return queryValue(String.class);
    }

    /**
     * @return The result of the view as int.
     */
    public int queryForInt() {
        return queryValue(int.class);
    }

    /**
     * @return The result of the view as long.
     */
    public long queryForLong() {
        return queryValue(long.class);
    }

    /**
     * @return The result of the view as boolean.
     */
    public boolean queryForBoolean() {
        return queryValue(boolean.class);
    }

    /**
     * Queries for scalar values. Internal use.
     */
    private <V> V queryValue(Class<V> classOfV) {
        InputStream instream = null;
        try {
            Reader reader = new InputStreamReader(instream = queryForStream(), StandardCharsets.UTF_8);
            JsonArray array = new JsonParser().parse(reader).getAsJsonObject().get("rows").getAsJsonArray();
            if (array.size() != 1) { // expect exactly 1 row
                throw new NoDocumentException(
                        "Expecting exactly a single result of this view query, but was: " + array.size());
            }
            return JsonToObject(gson, array.get(0), "value", classOfV);
        } finally {
            close(instream);
        }
    }

    /**
     * Queries a view for pagination, returns a next or a previous page, this
     * method figures out which page to return based on the given param that is
     * generated by an earlier call to this method, quering the first page is
     * done by passing a {@code null} param.
     * 
     * @param <T>
     *            Object type T
     * @param rowsPerPage
     *            The number of rows per page.
     * @param param
     *            The request parameter to use to query a page, or {@code null}
     *            to return the first page.
     * @param classOfT
     *            The class of type T.
     * @return {@link Page}
     */
    public <T> Page<T> queryPage(int rowsPerPage, String param, Class<T> classOfT) {
        if (param == null) { // assume first page
            return queryNextPage(rowsPerPage, null, null, null, null, classOfT);
        }
        String currentStartKey;
        String currentStartKeyDocId;
        String startKey;
        String startKeyDocId;
        String action;
        try {
            // extract fields from the returned HEXed JSON object
            JsonObject json = new JsonParser().parse(new String(Base64.decodeBase64(param.getBytes())))
                    .getAsJsonObject();
            if (log.isDebugEnabled()) {
                log.debug("Paging Param Decoded = " + json);
            }
            JsonObject jsonCurrent = json.getAsJsonObject(CURRENT_KEYS);
            currentStartKey = jsonCurrent.get(CURRENT_START_KEY).getAsString();
            currentStartKeyDocId = jsonCurrent.get(CURRENT_START_KEY_DOC_ID).getAsString();
            startKey = json.get(START_KEY).getAsString();
            startKeyDocId = json.get(START_KEY_DOC_ID).getAsString();
            action = json.get(ACTION).getAsString();
        } catch (Exception e) {
            throw new CouchDbException("could not parse the given param!", e);
        }
        if (PREVIOUS.equals(action)) { // previous
            return queryPreviousPage(rowsPerPage, currentStartKey, currentStartKeyDocId, startKey, startKeyDocId,
                    classOfT);
        } else { // next
            return queryNextPage(rowsPerPage, currentStartKey, currentStartKeyDocId, startKey, startKeyDocId,
                    classOfT);
        }
    }

    /**
     * @return The next page.
     */
    private <T> Page<T> queryNextPage(int rowsPerPage, String currentStartKey, String currentStartKeyDocId,
            String startKey, String startKeyDocId, Class<T> classOfT) {
        // set view query params
        limit(rowsPerPage + 1);
        includeDocs(true);
        if (startKey != null) {
            startKey(startKey);
            startKeyDocId(startKeyDocId);
        }
        // init page, query view
        Page<T> page = new Page<T>();
        List<T> pageList = new ArrayList<T>();
        ViewResult<String, String, T> vr = queryView(String.class, String.class, classOfT);
        List<ViewResult<String, String, T>.Rows> rows = vr.getRows();
        int resultRows = rows.size();
        int offset = vr.getOffset();
        long totalRows = vr.getTotalRows();
        // holds page params
        JsonObject currentKeys = new JsonObject();
        JsonObject jsonNext = new JsonObject();
        JsonObject jsonPrev = new JsonObject();
        currentKeys.addProperty(CURRENT_START_KEY, rows.get(0).getKey());
        currentKeys.addProperty(CURRENT_START_KEY_DOC_ID, rows.get(0).getId());
        for (int i = 0; i < resultRows; i++) {
            // set keys for the next page
            if (i == resultRows - 1) { // last element (i.e rowsPerPage + 1)
                if (resultRows > rowsPerPage) { // if not last page
                    page.setHasNext(true);
                    jsonNext.addProperty(START_KEY, rows.get(i).getKey());
                    jsonNext.addProperty(START_KEY_DOC_ID, rows.get(i).getId());
                    jsonNext.add(CURRENT_KEYS, currentKeys);
                    jsonNext.addProperty(ACTION, NEXT);
                    page.setNextParam(Base64.encodeBase64URLSafeString(jsonNext.toString().getBytes()));
                    continue; // exclude 
                }
            }
            pageList.add(rows.get(i).getDoc());
        }
        // set keys for the previous page
        if (offset != 0) { // if not first page
            page.setHasPrevious(true);
            jsonPrev.addProperty(START_KEY, currentStartKey);
            jsonPrev.addProperty(START_KEY_DOC_ID, currentStartKeyDocId);
            jsonPrev.add(CURRENT_KEYS, currentKeys);
            jsonPrev.addProperty(ACTION, PREVIOUS);
            page.setPreviousParam(Base64.encodeBase64URLSafeString(jsonPrev.toString().getBytes()));
        }
        // calculate paging display info
        page.setResultList(pageList);
        page.setTotalResults(totalRows);
        page.setResultFrom(offset + 1);
        int resultTo = rowsPerPage > resultRows ? resultRows : rowsPerPage; // fix when rowsPerPage exceeds returned rows
        page.setResultTo(offset + resultTo);
        page.setPageNumber((int) Math.ceil(page.getResultFrom() / Double.valueOf(rowsPerPage)));
        return page;
    }

    /**
     * @return The previous page.
     */
    private <T> Page<T> queryPreviousPage(int rowsPerPage, String currentStartKey, String currentStartKeyDocId,
            String startKey, String startKeyDocId, Class<T> classOfT) {
        // set view query params
        limit(rowsPerPage + 1);
        includeDocs(true);
        descending(true); // read backward
        startKey(currentStartKey);
        startKeyDocId(currentStartKeyDocId);
        // init page, query view
        Page<T> page = new Page<T>();
        List<T> pageList = new ArrayList<T>();
        ViewResult<String, String, T> vr = queryView(String.class, String.class, classOfT);
        List<ViewResult<String, String, T>.Rows> rows = vr.getRows();
        int resultRows = rows.size();
        int offset = vr.getOffset();
        long totalRows = vr.getTotalRows();
        Collections.reverse(rows); // fix order
        // holds page params
        JsonObject currentKeys = new JsonObject();
        JsonObject jsonNext = new JsonObject();
        JsonObject jsonPrev = new JsonObject();
        currentKeys.addProperty(CURRENT_START_KEY, rows.get(0).getKey());
        currentKeys.addProperty(CURRENT_START_KEY_DOC_ID, rows.get(0).getId());
        for (int i = 0; i < resultRows; i++) {
            // set keys for the next page
            if (i == resultRows - 1) { // last element (i.e rowsPerPage + 1)
                if (resultRows >= rowsPerPage) { // if not last page
                    page.setHasNext(true);
                    jsonNext.addProperty(START_KEY, rows.get(i).getKey());
                    jsonNext.addProperty(START_KEY_DOC_ID, rows.get(i).getId());
                    jsonNext.add(CURRENT_KEYS, currentKeys);
                    jsonNext.addProperty(ACTION, NEXT);
                    page.setNextParam(Base64.encodeBase64URLSafeString(jsonNext.toString().getBytes()));
                    continue;
                }
            }
            pageList.add(rows.get(i).getDoc());
        }
        // set keys for the previous page
        if (offset != (totalRows - rowsPerPage - 1)) { // if not first page
            page.setHasPrevious(true);
            jsonPrev.addProperty(START_KEY, currentStartKey);
            jsonPrev.addProperty(START_KEY_DOC_ID, currentStartKeyDocId);
            jsonPrev.add(CURRENT_KEYS, currentKeys);
            jsonPrev.addProperty(ACTION, PREVIOUS);
            page.setPreviousParam(Base64.encodeBase64URLSafeString(jsonPrev.toString().getBytes()));
        }
        // calculate paging display info
        page.setResultList(pageList);
        page.setTotalResults(totalRows);
        page.setResultFrom((int) totalRows - (offset + rowsPerPage));
        int resultTo = (int) totalRows - offset - 1;
        page.setResultTo(resultTo);
        page.setPageNumber(resultTo / rowsPerPage);
        return page;
    }

    // fields

    /**
     * @param key
     *            The key value, accepts a single value or multiple values for
     *            complex keys.
     */
    public View key(Object... key) {
        this.key = getKeyAsJson(key);
        uriBuilder.query("key", this.key);
        return this;
    }

    /**
     * @param startKey
     *            The start key value, accepts a single value or multiple values
     *            for complex keys.
     */
    public View startKey(Object... startKey) {
        this.startKey = getKeyAsJson(startKey);
        uriBuilder.query("startkey", this.startKey);
        return this;
    }

    public View startKeyDocId(String startKeyDocId) {
        this.startKeyDocId = startKeyDocId;
        uriBuilder.query("startkey_docid", this.startKeyDocId);
        return this;
    }

    /**
     * @param endKey
     *            The end key value, accepts a single value or multiple values
     *            for complex keys.
     */
    public View endKey(Object... endKey) {
        this.endKey = getKeyAsJson(endKey);
        uriBuilder.query("endkey", this.endKey);
        return this;
    }

    public View endKeyDocId(String endKeyDocId) {
        this.endKeyDocId = endKeyDocId;
        uriBuilder.query("endkey_docid", this.endKeyDocId);
        return this;
    }

    public View limit(Integer limit) {
        this.limit = limit;
        uriBuilder.query("limit", this.limit);
        return this;
    }

    /**
     * @param stale
     *            Accept values: ok | update_after (update_after as of CouchDB
     *            1.1.0)
     */
    public View stale(String stale) {
        this.stale = stale;
        uriBuilder.query("stale", this.stale);
        return this;
    }

    /**
     * Reverses the reading direction, not the sort order.
     */
    public View descending(Boolean descending) {
        this.descending = Boolean.valueOf(gson.toJson(descending));
        uriBuilder.query("descending", this.descending);
        return this;
    }

    /**
     * @param skip
     *            Skips <i>n</i> number of documents.
     */
    public View skip(Integer skip) {
        this.skip = skip;
        uriBuilder.query("skip", this.skip);
        return this;
    }

    /**
     * @param group
     *            Specifies whether the reduce function reduces the result to a
     *            set of keys, or to a single result. Defaults to false (single
     *            result).
     */
    public View group(Boolean group) {
        this.group = group;
        uriBuilder.query("group", this.group);
        return this;
    }

    public View groupLevel(Integer groupLevel) {
        this.groupLevel = groupLevel;
        uriBuilder.query("group_level", this.groupLevel);
        return this;
    }

    /**
     * @param reduce
     *            Indicates whether to use the reduce function of the view,
     *            defaults to true if the reduce function is defined.
     */
    public View reduce(Boolean reduce) {
        this.reduce = reduce;
        uriBuilder.query("reduce", this.reduce);
        return this;
    }

    public View includeDocs(Boolean includeDocs) {
        this.includeDocs = includeDocs;
        uriBuilder.query("include_docs", this.includeDocs);
        return this;
    }

    /**
     * @param inclusiveEnd
     *            Indicates whether the endkey is included in the result,
     *            defaults to true.
     */
    public View inclusiveEnd(Boolean inclusiveEnd) {
        this.inclusiveEnd = inclusiveEnd;
        uriBuilder.query("inclusive_end", this.inclusiveEnd);
        return this;
    }

    public View updateSeq(Boolean updateSeq) {
        this.updateSeq = updateSeq;
        uriBuilder.query("update_seq", this.updateSeq);
        return this;
    }

    public View keys(List<String> keys) {
        this.allDocsKeys = String.format("{%s:%s}", gson.toJson("keys"), gson.toJson(keys));
        return this;
    }

    // temp views

    public View tempView(String id) {
        assertNotEmpty(id, "id");
        String viewPath = format("%s/%s/", TEMP_VIEWS_DIR, id);
        List<String> dirList = listResources(viewPath);
        assertNotEmpty(dirList, "Temp view directory");

        tempView = new MapReduce();
        for (String mapRed : dirList) {
            String def = readFile(format("/%s%s", viewPath, mapRed));
            if (MAP_JS.equals(mapRed))
                tempView.setMap(def);
            else if (REDUCE_JS.equals(mapRed))
                tempView.setReduce(def);
        }
        return this;
    }

    public View tempView(MapReduce mapReduce) {
        assertNotEmpty(mapReduce, "mapReduce");
        tempView = mapReduce;
        return this;
    }

    private String getKeyAsJson(Object... key) {
        return (key.length == 1) ? gson.toJson(key[0]) : gson.toJson(key); // single or complex key
    }

    /**
     * Queries a view.
     * 
     * @param <T>
     *            Object type T
     * @param classOfT
     *            The class of type T
     * @return The result of the view query as a {@code List<T> }
     */
    public List<String> query() {
        InputStream instream = null;
        try {
            Reader reader = new InputStreamReader(instream = queryForStream(), StandardCharsets.UTF_8);
            JsonArray jsonArray = new JsonParser().parse(reader).getAsJsonObject().getAsJsonArray("rows");
            List<String> list = new ArrayList<String>();
            for (JsonElement jsonElem : jsonArray) {
                JsonElement elem = jsonElem.getAsJsonObject();
                if (Boolean.TRUE.equals(this.includeDocs)) {
                    elem = jsonElem.getAsJsonObject().get("doc");
                }
                String id = elem.toString();
                list.add(id);
            }
            return list;
        } finally {
            close(instream);
        }
    }
}