com.cloudant.client.api.Search.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudant.client.api.Search.java

Source

/*
 * Copyright (c) 2015 IBM Corp. All rights reserved.
 *
 * 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 com.cloudant.client.api;

import static com.cloudant.client.org.lightcouch.internal.CouchDbUtil.JsonToObject;
import static com.cloudant.client.org.lightcouch.internal.CouchDbUtil.assertNotEmpty;
import static com.cloudant.client.org.lightcouch.internal.CouchDbUtil.close;
import static com.cloudant.client.org.lightcouch.internal.CouchDbUtil.getAsLong;
import static com.cloudant.client.org.lightcouch.internal.CouchDbUtil.getAsString;
import static com.cloudant.client.org.lightcouch.internal.CouchDbUtil.getStream;

import com.cloudant.client.api.model.SearchResult;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.client.methods.HttpGet;
import com.cloudant.client.org.lightcouch.internal.URIBuilder;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * This class provides access to the Cloudant <tt>Search</tt> APIs.
 * <h3>Usage Example:</h3>
 * <pre>
 * {@code
 *  List<Bird> birds = db.search("views101/animals")
 *    .limit(10)
 *    .includeDocs(true)
 *    .query("class:bird", Bird.class);
 *
 * // groups
 * Map<String,List<Bird>> birdGroups = db.search("views101/animals")
 *    .limit(10)
 *    .includeDocs(true)
 *    .queryGroups("class:bird", Bird.class);
 * for ( Entry<String, List<Bird>> group : birdGroups.entrySet()) {
 *       System.out.println("Group Name : " +  group.getKey());
 *       for ( Bird b : group.getValue() ) {
 *           System.out.println("\t" + b);
 *        }
 *    }
 *
 *  // search results object
 *  SearchResult<Bird> result = db.search("views101/animals")
 *   .querySearchResult("class:bird", Bird.class);
 *
 *  // pagination
 *  SearchResult<Bird> nextPage = db.search("views101/animals")
 *   .bookmark(result.bookmark)
 *   .querySearchResult("class:bird", Bird.class);
 * }
 * </pre>
 *
 * @author Mario Briggs
 * @see Database#search(String)
 * @see SearchResult
 * @since 0.0.1
 */
public class Search {

    private static final Log log = LogFactory.getLog(Search.class);

    // search fields
    private Integer limit;
    private boolean includeDocs = false;
    private String bookmark;
    private Database db;
    private URIBuilder uriBuilder;

    Search(Database db, String searchIndexId) {
        assertNotEmpty(searchIndexId, "searchIndexId");
        this.db = db;
        String search = searchIndexId;
        if (searchIndexId.contains("/")) {
            String[] v = searchIndexId.split("/");
            search = String.format("_design/%s/_search/%s", v[0], v[1]);
        }
        this.uriBuilder = URIBuilder.buildUri(db.getDBUri()).path(search);
    }

    // Query options

    /**
     * Performs a Cloudant Search and returns the result as an {@link InputStream}
     *
     * @param query the Lucene query to be passed to the Search index
     *              <p>The stream should be properly closed after usage, as to avoid connection
     *              leaks.
     * @return The result as an {@link InputStream}.
     */
    public InputStream queryForStream(String query) {
        key(query);
        URI uri = uriBuilder.build();
        HttpGet get = new HttpGet(uri);
        get.addHeader("Accept", "application/json");
        return getStream(db.executeRequest(get));
    }

    /**
     * Queries a Search Index and returns ungrouped results. In case the query
     * used grouping, an empty list is returned
     *
     * @param <T>      Object type T
     * @param query    the Lucene query to be passed to the Search index
     * @param classOfT The class of type T
     * @return The result of the search query as a {@code List<T> }
     */
    public <T> List<T> query(String query, Class<T> classOfT) {
        InputStream instream = null;
        List<T> result = new ArrayList<T>();
        try {
            Reader reader = new InputStreamReader(instream = queryForStream(query), "UTF-8");
            JsonObject json = new JsonParser().parse(reader).getAsJsonObject();
            if (json.has("rows")) {
                if (!includeDocs) {
                    log.warn("includeDocs set to false and attempting to retrieve doc. "
                            + "null object will be returned");
                }
                for (JsonElement e : json.getAsJsonArray("rows")) {
                    result.add(JsonToObject(db.getGson(), e, "doc", classOfT));
                }
            } else {
                log.warn("No ungrouped result available. Use queryGroups() if grouping set");
            }
            return result;
        } catch (UnsupportedEncodingException e1) {
            // This should never happen as every implementation of the java platform is required
            // to support UTF-8.
            throw new RuntimeException(e1);
        } finally {
            close(instream);
        }
    }

    /**
     * Queries a Search Index and returns grouped results in a map where key
     * of the map is the groupName. In case the query didnt use grouping,
     * an empty map is returned
     *
     * @param <T>      Object type T
     * @param query    the Lucene query to be passed to the Search index
     * @param classOfT The class of type T
     * @return The result of the grouped search query as a ordered {@code Map<String,T> }
     */
    public <T> Map<String, List<T>> queryGroups(String query, Class<T> classOfT) {
        InputStream instream = null;
        try {
            Reader reader = new InputStreamReader(instream = queryForStream(query), "UTF-8");
            JsonObject json = new JsonParser().parse(reader).getAsJsonObject();
            Map<String, List<T>> result = new LinkedHashMap<String, List<T>>();
            if (json.has("groups")) {
                for (JsonElement e : json.getAsJsonArray("groups")) {
                    String groupName = e.getAsJsonObject().get("by").getAsString();
                    List<T> orows = new ArrayList<T>();
                    if (!includeDocs) {
                        log.warn("includeDocs set to false and attempting to retrieve doc. "
                                + "null object will be returned");
                    }
                    for (JsonElement rows : e.getAsJsonObject().getAsJsonArray("rows")) {
                        orows.add(JsonToObject(db.getGson(), rows, "doc", classOfT));
                    }
                    result.put(groupName, orows);
                } // end for(groups)
            } // end hasgroups
            else {
                log.warn("No grouped results available. Use query() if non grouped query");
            }
            return result;
        } catch (UnsupportedEncodingException e1) {
            // This should never happen as every implementation of the java platform is required
            // to support UTF-8.
            throw new RuntimeException(e1);
        } finally {
            close(instream);
        }
    }

    /**
     * Performs a Cloudant Search and returns the result as an {@link SearchResult}
     *
     * @param <T>      Object type T, an instance into which the rows[].doc/group[].rows[].doc
     *                 attribute of the Search result response should be deserialized into. Same
     *                 goes for the rows[].fields/group[].rows[].fields attribute
     * @param query    the Lucene query to be passed to the Search index
     * @param classOfT The class of type T.
     * @return The Search result entries
     */
    public <T> SearchResult<T> querySearchResult(String query, Class<T> classOfT) {
        InputStream instream = null;
        try {
            Reader reader = new InputStreamReader(instream = queryForStream(query), "UTF-8");
            JsonObject json = new JsonParser().parse(reader).getAsJsonObject();
            SearchResult<T> sr = new SearchResult<T>();
            sr.setTotalRows(getAsLong(json, "total_rows"));
            sr.setBookmark(getAsString(json, "bookmark"));
            if (json.has("rows")) {
                sr.setRows(getRows(json.getAsJsonArray("rows"), sr, classOfT));
            } else if (json.has("groups")) {
                setGroups(json.getAsJsonArray("groups"), sr, classOfT);
            }

            if (json.has("counts")) {
                sr.setCounts(getFieldsCounts(json.getAsJsonObject("counts").entrySet()));
            }

            if (json.has("ranges")) {
                sr.setRanges(getFieldsCounts(json.getAsJsonObject("ranges").entrySet()));
            }
            return sr;
        } catch (UnsupportedEncodingException e) {
            // This should never happen as every implementation of the java platform is required
            // to support UTF-8.
            throw new RuntimeException(e);
        } finally {
            close(instream);
        }
    }

    /**
     * @param limit limit the number of documents in the result
     */
    public Search limit(Integer limit) {
        this.limit = limit;
        uriBuilder.query("limit", this.limit);
        return this;
    }

    /**
     * Control which page of results to get. The bookmark value is obtained by executing
     * the query()/queryForStream() once and getting it from the bookmark field
     * in the response
     *
     * @param bookmark see the next page of results after this bookmark result
     * @return
     */
    public Search bookmark(String bookmark) {
        this.bookmark = bookmark;
        uriBuilder.query("bookmark", this.bookmark);
        return this;
    }

    /**
     * Specify the sort order for the result.
     *
     * @param sortJson @see <a href="http://docs.cloudant.com/api/search.html"> sort</a>
     *                 query argument for format
     * @return
     */
    public Search sort(String sortJson) {
        assertNotEmpty(sortJson, "sort");
        uriBuilder.query("sort", sortJson);
        return this;
    }

    /**
     * field by which to group results.
     *
     * @param fieldName
     * @param isNumber  whether field isNumeric.
     * @return
     */
    public Search groupField(String fieldName, boolean isNumber) {
        assertNotEmpty(fieldName, "fieldName");
        if (isNumber) {
            uriBuilder.query("group_field", fieldName + "<number>");
        } else {
            uriBuilder.query("group_field", fieldName);
        }
        return this;
    }

    /**
     * Maximum group count when groupField is set
     *
     * @param limit
     * @return
     */
    public Search groupLimit(int limit) {
        uriBuilder.query("group_limit", limit);
        return this;
    }

    /**
     * the sort order of the groups when groupField is set
     *
     * @param groupsortJson @see <a href="http://docs.cloudant.com/api/search.html"> sort</a>
     *                      query argument here for format
     * @return
     */
    public Search groupSort(String groupsortJson) {
        assertNotEmpty(groupsortJson, "groupsortJson");
        uriBuilder.query("group_sort", groupsortJson);
        return this;
    }

    /**
     * Ranges for faceted searches.  @see <a href="http://docs.cloudant.com/api/search.html">
     *     ranges</a>
     * query argument here for format
     *
     * @param rangesJson
     * @return
     */
    public Search ranges(String rangesJson) {
        assertNotEmpty(rangesJson, "rangesJson");
        uriBuilder.query("ranges", rangesJson);
        return this;
    }

    /**
     * Array of fieldNames for which counts should be produced
     *
     * @param countsfields
     * @return
     */
    public Search counts(String[] countsfields) {
        assert (countsfields.length > 0);
        int i = 0;
        String counts = "[";
        for (; i < countsfields.length - 1; i++) {
            counts += "\"" + countsfields[i] + "\",";
        }
        counts += "\"" + countsfields[i] + "\"]";
        uriBuilder.query("counts", counts);
        return this;
    }

    /**
     * @param fieldName
     * @param fieldValue
     * @return
     * @see <a href="http://docs.cloudant.com/api/search.html"> drilldown</a>
     * query argument
     */
    public Search drillDown(String fieldName, String fieldValue) {
        assertNotEmpty(fieldName, "fieldName");
        assertNotEmpty(fieldValue, "fieldValue");
        uriBuilder.query("drilldown", "[\"" + fieldName + "\",\"" + fieldValue + "\"]");
        return this;
    }

    /**
     * @param stale Accept values: ok
     */
    public Search stale(boolean stale) {
        if (stale) {
            uriBuilder.query("stale", "ok");
        }
        return this;
    }

    /**
     * @param includeDocs whether to include the document in the result
     */
    public Search includeDocs(Boolean includeDocs) {
        this.includeDocs = includeDocs;
        uriBuilder.query("include_docs", this.includeDocs);
        return this;
    }

    private void key(String query) {
        uriBuilder.query("q", query);
    }

    private Map<String, Map<String, Long>> getFieldsCounts(Set<Map.Entry<String, JsonElement>> fldset) {
        Map<String, Map<String, Long>> map = new HashMap<String, Map<String, Long>>();
        for (Entry<String, JsonElement> fld : fldset) {
            String field = fld.getKey();
            Map<String, Long> ovalues = new HashMap<String, Long>();
            if (fld.getValue().isJsonObject()) {
                Set<Map.Entry<String, JsonElement>> values = fld.getValue().getAsJsonObject().entrySet();
                for (Entry<String, JsonElement> value : values) {
                    ovalues.put(value.getKey(), value.getValue().getAsLong());
                }
            }
            map.put(field, ovalues);
        }
        return map;
    }

    private <T> List<SearchResult<T>.SearchResultRows> getRows(JsonArray jsonrows, SearchResult<T> sr,
            Class<T> classOfT) {

        List<SearchResult<T>.SearchResultRows> ret = new ArrayList<SearchResult<T>.SearchResultRows>();
        for (JsonElement e : jsonrows) {
            SearchResult<T>.SearchResultRows row = sr.new SearchResultRows();
            JsonObject oe = e.getAsJsonObject();
            row.setId(oe.get("id").getAsString());
            row.setOrder(JsonToObject(db.getGson(), e, "order", Object[].class));
            row.setFields(JsonToObject(db.getGson(), e, "fields", classOfT));
            if (includeDocs) {
                row.setDoc(JsonToObject(db.getGson(), e, "doc", classOfT));
            }
            ret.add(row);
        }
        return ret;
    }

    private <T> void setGroups(JsonArray jsongroups, SearchResult<T> sr, Class<T> classOfT) {
        for (JsonElement e : jsongroups) {
            SearchResult<T>.SearchResultGroups group = sr.new SearchResultGroups();
            JsonObject oe = e.getAsJsonObject();
            group.setBy(oe.get("by").getAsString());
            group.setTotalRows(oe.get("total_rows").getAsLong());
            group.setRows(getRows(oe.getAsJsonArray("rows"), sr, classOfT));
            sr.getGroups().add(group);
        }
    }
}