package ru.dreamteam.couch.query;
import static ru.dreamteam.couch.CouchConstants.DESCENDING;
import static ru.dreamteam.couch.CouchConstants.END_KEY;
import static ru.dreamteam.couch.CouchConstants.GROUP;
import static ru.dreamteam.couch.CouchConstants.INCLUDE_DOCS;
import static ru.dreamteam.couch.CouchConstants.KEY;
import static ru.dreamteam.couch.CouchConstants.LIMIT;
import static ru.dreamteam.couch.CouchConstants.SKIP;
import static ru.dreamteam.couch.CouchConstants.START_DOCID;
import static ru.dreamteam.couch.CouchConstants.START_KEY;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import javax.xml.crypto.MarshalException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.utils.URIBuilder;
import static org.apache.http.HttpStatus.*;
import ru.dreamteam.couch.Couch;
import ru.dreamteam.couch.Db;
import ru.dreamteam.couch.HttpCall;
import ru.dreamteam.couch.MarshallingException;
import ru.dreamteam.couch.PagingResult;
import ru.dreamteam.couch.util.JSONUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ArrayNode;
/**
* Base class for a couchdb query. Use {@link Db#query(String, String, Class)} to create a new query.
* All options represent couch db query parameters - http://wiki.apache.org/couchdb/HTTP_view_API#Querying_Options
*
* User: DPokidov
* Date: 02.01.14
*/
public class Query<T> {
private final Logger log = Logger.getLogger(Query.class.getName());
private Class<T> clazz;
private Couch dbInstance;
private String uriPath;
private ObjectMapper mapper;
private ObjectWriter jsonWriter;
private ObjectReader jsonReader;
private Map<String, String> queryParams;
/**
* Create a new query to a database.
* Common usage case is to using {@link Db#query(String, String, Class)} method to
* create query.
* @param dbInstance database instance.
* @param uriPath path to view.
* @param clazz result class.
*/
public Query(Couch dbInstance, String uriPath, Class<T> clazz) {
this.clazz = clazz;
this.dbInstance = dbInstance;
this.uriPath = uriPath;
this.mapper = JSONUtils.createMapper();
this.jsonWriter = mapper.writer(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED);
this.jsonReader = mapper.reader(clazz);
this.queryParams = new HashMap<>();
}
/**
* Sets a start key for a query. Accepts array if a key in a view is complex.
* Note: if you need to sort result descending then you must swap your start key and end key.<br/>
* HINT: If you need to add empty key({}) then you can use {@code new Object()} as a key
* @param startKey start key for the query.
* @return current query.
*/
public Query<T> startKey(Object... startKey) {
putArrayQueryParam(START_KEY, startKey);
return this;
}
/**
* Sets a start key for a query. Accepts array if a key in a view is complex.
* Note: if you need to sort result descending then you must swap your start key and end key.<br/>
* HINT: If you need to add empty key({}) then you can use {@code new Object()} as a key
* @param startKey end key for the query.
* @return current query.
*/
public Query<T> endKey(Object... endKey) {
putArrayQueryParam(END_KEY, endKey);
return this;
}
/**
* Sets a key for a query. Accepts array if a key in a view is complex.
* @param key key for the query.
* @return current query.
*/
public Query<T> key(Object... key) {
putArrayQueryParam(KEY, key);
return this;
}
private void putArrayQueryParam(String paramName, Object... values) {
if (values != null && values.length > 0) {
try {
queryParams.put(paramName, jsonWriter.writeValueAsString(values));
} catch (JsonProcessingException e) {
throw new MarshallingException("Error while serialize [" + values.toString() + "]", e);
}
}
}
/**
* Sets a number of documents to skip from beginning of result.
* @param skip number of records to skip. Must be greater then 0.
* @return current query.
*/
public Query<T> skip(int skip) {
if (skip > 0) {
queryParams.put(SKIP, Integer.toString(skip));
}
return this;
}
/**
* Sets a limit for a number of documents that will be returned.
* @param limit maximum number of document.
* @return current query.
*/
public Query<T> limit(int limit) {
if (limit > 0) {
queryParams.put(LIMIT, Integer.toString(limit));
}
return this;
}
/**
* Sets a document id from which result will begin.
* All documents before this will be ignored.
* @param startKeyDocId Start document id. Must not be {@code null}
* @return current query.
*/
public Query<T> startKeyDocId(String startKeyDocId) {
if (startKeyDocId != null) {
queryParams.put(START_DOCID, startKeyDocId);
}
return this;
}
/**
* Sets a direction of sorting documents.<br/>
* By default documents sorting ascending by change key.
* @param descending New direction of sorting. {@code true} for descending, {@code false} for ascending.
* @return current query.
*/
public Query<T> descending(boolean descending) {
queryParams.put(DESCENDING, Boolean.toString(descending));
return this;
}
/**
* If group param is true, then CouchDb will call reduce function in view.
* @param group Group param
* @return current query
*/
public Query<T> group(boolean group) {
queryParams.put(GROUP, Boolean.toString(group));
return this;
}
/**
* If includeDocs param set to {@code true} then CouchDb will include documents in response.<br/>
* By default set to {@code true}
* @param includeDocs IncludeDocs param
* @return current query
*/
public Query<T> includeDocs(boolean includeDocs) {
queryParams.put(INCLUDE_DOCS, Boolean.toString(includeDocs));
return this;
}
/**
* Executes query and returns a list of result documents.
* @return result documents
*/
public PagingResult<T> list() {
return dbInstance.execute(new HttpCall<PagingResult<T>>() {
@Override
public HttpRequest getRequest() throws URISyntaxException {
return new HttpGet(buildQueryUri());
}
@Override
public PagingResult<T> doWithResponse(HttpResponse response) throws IOException {
if (response.getStatusLine().getStatusCode() == SC_NOT_FOUND) {
throw new CouchQueryException("View [" + uriPath + "] does not exists");
}
PagingResult<T> result = new PagingResult<>();
JsonNode node = mapper.readTree(response.getEntity().getContent());
if (node.get("total_rows") != null) {
result.setTotalRows(node.get("total_rows").asInt());
}
ArrayNode rowsNode = (ArrayNode) node.get("rows");
for (JsonNode aRowsNode : rowsNode) {
JsonNode valueNode = aRowsNode.has("doc") ? aRowsNode.get("doc") : aRowsNode.get("value");
if (!clazz.getName().equals(String.class.getName())) {
result.add((T) jsonReader.readValue(valueNode));
} else {
result.add((T) valueNode.toString());
}
}
return result;
}
}, SC_OK, SC_NOT_FOUND);
}
/**
* Return number of documents for current query.
* @return number of documents
*/
public int count() {
return dbInstance.execute(new HttpCall<Integer>() {
@Override
public HttpRequest getRequest() throws URISyntaxException {
return new HttpGet(buildQueryUri());
}
@Override
public Integer doWithResponse(HttpResponse response) throws IOException {
if (response.getStatusLine().getStatusCode() == SC_NOT_FOUND) {
throw new CouchQueryException("View [" + uriPath + "] does not exists");
}
PagingResult<T> result = new PagingResult<>();
JsonNode node = mapper.readTree(response.getEntity().getContent());
if (node.get("total_rows") != null) {
result.setTotalRows(node.get("total_rows").asInt());
}
ArrayNode rowsNode = (ArrayNode) node.get("rows");
return rowsNode.size();
}
}, SC_OK, SC_NOT_FOUND);
}
/**
* Expects only one object in a query result
* Shorthand for {@code list().get(0)}
* @return result document or {@code null} if object not found
* @throws CouchQueryException if more then one object in result
*/
public T one() throws CouchQueryException {
List<T> list = list();
if (list.size() > 1) {
throw new CouchQueryException("Expected 1 object in result, actually [" + list.size() + "]");
}
return list.isEmpty() ? null : list.get(0);
}
private URI buildQueryUri() throws URISyntaxException {
URIBuilder uriBuilder = new URIBuilder();
uriBuilder.setPath(uriPath);
for (Map.Entry<String, String> opt : queryParams.entrySet()) {
uriBuilder.setParameter(opt.getKey(), opt.getValue());
}
return uriBuilder.build();
}
}