From f502b1f261093d07b5c8483ee9764c050670911b Mon Sep 17 00:00:00 2001 From: Hendy Irawan Date: Mon, 14 Jul 2014 00:29:30 -0430 Subject: [PATCH] Implementation of the `executeQuery` method of `StdCouchDbConnector`, from the discussion in https://github.com/helun/Ektorp/issues/165, which prefers `GET` HTTP method even in case of multiple keys. It is more appropriate for hosted services like Cloudant where `POST` requests are charged more than `GET`. However, if the HTTP request length exceeds `MAX_KEYS_LENGTH_FOR_GET` characters, it will use `POST` HTTP method. `ViewQuery` needed to be factored slightly to support its usage. --- org.ektorp/pom.xml | 5 +- .../src/main/java/org/ektorp/ViewQuery.java | 77 ++++++++++++++++--- .../org/ektorp/impl/DefaultQueryExecutor.java | 8 +- .../ektorp/impl/PreferGetQueryExecutor.java | 77 +++++++++++++++++++ 4 files changed, 150 insertions(+), 17 deletions(-) create mode 100644 org.ektorp/src/main/java/org/ektorp/impl/PreferGetQueryExecutor.java diff --git a/org.ektorp/pom.xml b/org.ektorp/pom.xml index 358701ac..8c354c77 100644 --- a/org.ektorp/pom.xml +++ b/org.ektorp/pom.xml @@ -57,8 +57,9 @@ runtime - org.slf4j - slf4j-simple + ch.qos.logback + logback-classic + 1.1.2 test diff --git a/org.ektorp/src/main/java/org/ektorp/ViewQuery.java b/org.ektorp/src/main/java/org/ektorp/ViewQuery.java index 209b7886..5665cfb0 100644 --- a/org.ektorp/src/main/java/org/ektorp/ViewQuery.java +++ b/org.ektorp/src/main/java/org/ektorp/ViewQuery.java @@ -8,15 +8,18 @@ import java.util.Map; import java.util.TreeMap; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import org.ektorp.http.URI; +import org.ektorp.impl.DefaultQueryExecutor; +import org.ektorp.impl.QueryExecutor; import org.ektorp.impl.StdObjectMapperFactory; import org.ektorp.util.Assert; import org.ektorp.util.Exceptions; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + /** * * @author henrik lundgren @@ -263,9 +266,12 @@ public ViewQuery key(Object o) { return this; } /** - * For multiple-key queries (as of CouchDB 0.9). Keys will be JSON-encoded. + * For multiple-key queries (as of CouchDB 0.9), the HTTP method is determined by {@link QueryExecutor}. + * Keys will be JSON-encoded. * @param keyList a list of Object, will be JSON encoded according to each element's type. * @return the view query for chained calls + * @see DefaultQueryExecutor + * @see For ViewQuery.keys(), use GET instead of POST */ public ViewQuery keys(Collection keyList) { reset(); @@ -567,7 +573,12 @@ public Object getKey() { public boolean hasMultipleKeys() { return keys != null; } - + + /** + * Get {@link #keys} as JSON object. + * @return + * @see #getKeysAsJsonArray() + */ public String getKeysAsJson() { if (keys == null) { return "{\"keys\":[]}"; @@ -575,6 +586,17 @@ public String getKeysAsJson() { return keys.toJson(mapper); } + /** + * Get {@link #keys} as JSON array. + * @return + * @see #getKeysAsJson() + */ + public String getKeysAsJsonArray() { + if (keys == null) { + return "[]"; + } + return keys.toJsonArray(mapper); + } public Object getStartKey() { return startKey; @@ -584,23 +606,33 @@ public Object getEndKey() { return endKey; } - public String buildQuery() { + /** + * Builds the HTTP query. + * @param keysAsJsonArray If not {@code null} (typically from {@link #getKeysAsJsonArray()}), + * it will be included as {@code keys} HTTP query parameter. + * @return + */ + public String buildQuery(String keysAsJsonArray) { if (cachedQuery != null) { return cachedQuery; } - URI query = buildQueryURI(); + URI query = buildQueryURI(keysAsJsonArray); cachedQuery = query.toString(); return cachedQuery; } - public URI buildQueryURI() { + public URI buildQueryURI(String keysAsJsonArray) { URI query = buildViewPath(); if (isNotEmpty(key)) { query.param("key", jsonEncode(key)); } + + if (keysAsJsonArray != null) { + query.param("keys", keysAsJsonArray); + } if (isNotEmpty(startKey)) { query.param("startkey", jsonEncode(startKey)); @@ -664,7 +696,16 @@ public URI buildQueryURI() { return query; } - @edu.umd.cs.findbugs.annotations.SuppressWarnings({"SA_FIELD_SELF_ASSIGNMENT", "CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE"}) + /** + * Builds the HTTP query without including {@link #keys(Collection)}). + * @return + */ + public String buildQuery() { + return buildQuery(null); + } + + @Override + @edu.umd.cs.findbugs.annotations.SuppressWarnings({"SA_FIELD_SELF_ASSIGNMENT", "CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE"}) public ViewQuery clone() { ViewQuery copy = new ViewQuery(); copy.mapper = mapper; @@ -905,7 +946,8 @@ public List getValues() { return Collections.unmodifiableList(keys); } - @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE") + @Override + @edu.umd.cs.findbugs.annotations.SuppressWarnings(value="CN_IMPLEMENTS_CLONE_BUT_NOT_CLONEABLE") public Keys clone() { return new Keys(keys); } @@ -926,8 +968,19 @@ public String toJson(ObjectMapper mapper) { throw Exceptions.propagate(e); } } - } + public String toJsonArray(ObjectMapper mapper) { + ArrayNode keysNode = mapper.createArrayNode(); + for (Object key : keys) { + keysNode.addPOJO(key); + } + try { + return mapper.writeValueAsString(keysNode); + } catch (Exception e) { + throw Exceptions.propagate(e); + } + } + } } diff --git a/org.ektorp/src/main/java/org/ektorp/impl/DefaultQueryExecutor.java b/org.ektorp/src/main/java/org/ektorp/impl/DefaultQueryExecutor.java index 1de83bee..9a864963 100644 --- a/org.ektorp/src/main/java/org/ektorp/impl/DefaultQueryExecutor.java +++ b/org.ektorp/src/main/java/org/ektorp/impl/DefaultQueryExecutor.java @@ -10,10 +10,12 @@ * This is the default implementation of the executeQuery method of StdCouchDbConnector, * as of before the method was delegating to the QueryExecutor strategy interface. * - * Be aware that, as stated in https://github.com/helun/Ektorp/issues/165 this implementation is making use of POST HTTP method in case of multiple keys, - * so that it may not be appropriate for hosted services like Cloudant where POST are more charged that GET. + * Be aware that, as stated in https://github.com/helun/Ektorp/issues/165 this + * implementation is making use of {@code POST} HTTP method in case of multiple keys, + * so that it may not be appropriate for hosted services like Cloudant + * where {@code POST} requests are charged more than {@code GET}. * -*/ + */ public class DefaultQueryExecutor implements QueryExecutor { /** diff --git a/org.ektorp/src/main/java/org/ektorp/impl/PreferGetQueryExecutor.java b/org.ektorp/src/main/java/org/ektorp/impl/PreferGetQueryExecutor.java new file mode 100644 index 00000000..961676f5 --- /dev/null +++ b/org.ektorp/src/main/java/org/ektorp/impl/PreferGetQueryExecutor.java @@ -0,0 +1,77 @@ +package org.ektorp.impl; + +import org.ektorp.ViewQuery; +import org.ektorp.http.ResponseCallback; +import org.ektorp.http.RestTemplate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the {@code executeQuery} method of {@link StdCouchDbConnector}, + * from the discussion in Ektorp #165, + * which prefers {@code GET} HTTP method even in case of multiple keys. + * It is more appropriate for hosted services like Cloudant + * where {@code POST} requests are charged more than {@code GET}. + * + *

However, if the HTTP request length exceeds {@value #MAX_KEYS_LENGTH_FOR_GET} characters, + * it will use {@code POST} HTTP method. + * + * @author Hendy Irawan + */ +public class PreferGetQueryExecutor implements QueryExecutor { + + private static final Logger LOG = LoggerFactory.getLogger(PreferGetQueryExecutor.class); + + /** + * Maximum length of {@link ViewQuery#getKeysAsJsonArray()} for a + * {@code GET} HTTP request in {@link #executeQuery(ViewQuery, ResponseCallback)}, + * otherwise uses {@code POST}. + */ + public static final int MAX_KEYS_LENGTH_FOR_GET = 3000; + + private RestTemplate restTemplate; + + public PreferGetQueryExecutor() { + super(); + } + + public PreferGetQueryExecutor(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + public RestTemplate getRestTemplate() { + return restTemplate; + } + + public void setRestTemplate(RestTemplate value) { + this.restTemplate = value; + } + + @Override + public T executeQuery(ViewQuery query, ResponseCallback rh) { + LOG.debug("Querying CouchDb view at {}.", query); + T result; + if (!query.isCacheOk()) { + if (query.hasMultipleKeys()) { + final String keysAsJsonArray = query.getKeysAsJsonArray(); + result = keysAsJsonArray.length() > MAX_KEYS_LENGTH_FOR_GET + ? restTemplate.postUncached(query.buildQuery(), "{\"keys\":" + keysAsJsonArray + "}", rh) + : restTemplate.getUncached(query.buildQuery(keysAsJsonArray), rh); + } else { + result = restTemplate.getUncached(query.buildQuery(), rh); + } + } else { + if (query.hasMultipleKeys()) { + final String keysAsJsonArray = query.getKeysAsJsonArray(); + result = keysAsJsonArray.length() > MAX_KEYS_LENGTH_FOR_GET + ? restTemplate.post(query.buildQuery(), "{\"keys\":" + keysAsJsonArray + "}", rh) + : restTemplate.get(query.buildQuery(keysAsJsonArray), rh); + } else { + result = restTemplate.get(query.buildQuery(), rh); + } + } + LOG.debug("Answer from view query: {}.", result); + return result; + } + +}