diff --git a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java index 22f9c68c..9dd7eaff 100644 --- a/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java +++ b/constructorio-client/src/main/java/io/constructor/client/ConstructorIO.java @@ -926,8 +926,11 @@ public String retrieveVariationsAsJson(VariationsRequest req) throws Constructor public AutocompleteResponse autocomplete(AutocompleteRequest req, UserInfo userInfo) throws ConstructorException { try { - String json = autocompleteAsJSON(req, userInfo); - return createAutocompleteResponse(json); + Request request = createAutocompleteRequest(req, userInfo); + Response response = clientWithRetry.newCall(request).execute(); + Map> headers = response.headers().toMultimap(); + String json = getResponseBody(response); + return createAutocompleteResponse(json, headers); } catch (Exception exception) { throw new ConstructorException(exception); } @@ -947,6 +950,25 @@ public AutocompleteResponse autocomplete(AutocompleteRequest req, UserInfo userI */ public String autocompleteAsJSON(AutocompleteRequest req, UserInfo userInfo) throws ConstructorException { + try { + Request request = createAutocompleteRequest(req, userInfo); + Response response = clientWithRetry.newCall(request).execute(); + return getResponseBody(response); + } catch (Exception exception) { + throw new ConstructorException(exception); + } + } + + /** + * Creates an autocomplete OkHttp request + * + * @param req the autocomplete request + * @param userInfo optional information about the user + * @return an autocomplete OkHttp request + * @throws ConstructorException + */ + protected Request createAutocompleteRequest(AutocompleteRequest req, UserInfo userInfo) + throws ConstructorException { try { List paths = Arrays.asList("autocomplete", req.getQuery()); HttpUrl url = (userInfo == null) ? this.makeUrl(paths) : this.makeUrl(paths, userInfo); @@ -1009,10 +1031,7 @@ public String autocompleteAsJSON(AutocompleteRequest req, UserInfo userInfo) .build(); } - Request request = this.makeUserRequestBuilder(userInfo).url(url).get().build(); - - Response response = clientWithRetry.newCall(request).execute(); - return getResponseBody(response); + return this.makeUserRequestBuilder(userInfo).url(url).get().build(); } catch (Exception exception) { throw new ConstructorException(exception); } @@ -1171,8 +1190,9 @@ public SearchResponse search(SearchRequest req, UserInfo userInfo) throws Constr try { Request request = createSearchRequest(req, userInfo); Response response = clientWithRetry.newCall(request).execute(); + Map> headers = response.headers().toMultimap(); String json = getResponseBody(response); - return createSearchResponse(json); + return createSearchResponse(json, headers); } catch (Exception exception) { throw new ConstructorException(exception); } @@ -1207,8 +1227,10 @@ public void onFailure(Call call, IOException e) { public void onResponse(Call call, final Response response) throws IOException { try { + Map> headers = + response.headers().toMultimap(); String json = getResponseBody(response); - SearchResponse res = createSearchResponse(json); + SearchResponse res = createSearchResponse(json, headers); c.onResponse(res); } catch (Exception e) { c.onFailure(new ConstructorException(e)); @@ -2200,6 +2222,11 @@ protected String getVersion() { * to do it in a Gson Type Adapter. */ protected static AutocompleteResponse createAutocompleteResponse(String string) { + return createAutocompleteResponse(string, null); + } + + protected static AutocompleteResponse createAutocompleteResponse( + String string, Map> headers) { JSONObject json = new JSONObject(string); JSONObject sections = json.getJSONObject("sections"); for (Object sectionKey : sections.keySet()) { @@ -2208,7 +2235,11 @@ protected static AutocompleteResponse createAutocompleteResponse(String string) moveMetadataOutOfResultData(results); } String transformed = json.toString(); - return new Gson().fromJson(transformed, AutocompleteResponse.class); + AutocompleteResponse autocompleteResponse = + new Gson().fromJson(transformed, AutocompleteResponse.class); + autocompleteResponse.setHeaders(headers); + + return autocompleteResponse; } /** @@ -2217,6 +2248,11 @@ protected static AutocompleteResponse createAutocompleteResponse(String string) * in a Gson Type Adapter. */ protected static SearchResponse createSearchResponse(String string) { + return createSearchResponse(string, null); + } + + protected static SearchResponse createSearchResponse( + String string, Map> headers) { JSONObject json = new JSONObject(string); JSONObject response = json.getJSONObject("response"); JSONArray results; @@ -2227,7 +2263,9 @@ protected static SearchResponse createSearchResponse(String string) { } String transformed = json.toString(); - return new Gson().fromJson(transformed, SearchResponse.class); + SearchResponse searchResponse = new Gson().fromJson(transformed, SearchResponse.class); + searchResponse.setHeaders(headers); + return searchResponse; } /** diff --git a/constructorio-client/src/main/java/io/constructor/client/models/AutocompleteResponse.java b/constructorio-client/src/main/java/io/constructor/client/models/AutocompleteResponse.java index 412ec04a..1a9b78bd 100644 --- a/constructorio-client/src/main/java/io/constructor/client/models/AutocompleteResponse.java +++ b/constructorio-client/src/main/java/io/constructor/client/models/AutocompleteResponse.java @@ -5,7 +5,7 @@ import java.util.Map; /** Constructor.io Autocomplete Response ... uses Gson/Reflection to load data in */ -public class AutocompleteResponse { +public class AutocompleteResponse extends ResponseHeaders { @SerializedName("sections") private Map> sections; diff --git a/constructorio-client/src/main/java/io/constructor/client/models/ResponseHeaders.java b/constructorio-client/src/main/java/io/constructor/client/models/ResponseHeaders.java new file mode 100644 index 00000000..a1323296 --- /dev/null +++ b/constructorio-client/src/main/java/io/constructor/client/models/ResponseHeaders.java @@ -0,0 +1,26 @@ +package io.constructor.client.models; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** Base class that provides access to HTTP response headers. */ +public abstract class ResponseHeaders { + + private transient Map> headers = + Collections.>emptyMap(); + + /** + * @return the HTTP response headers + */ + public Map> getHeaders() { + return headers; + } + + /** + * @param headers the HTTP response headers to set. If null, defaults to an empty map. + */ + public void setHeaders(Map> headers) { + this.headers = (headers != null) ? headers : Collections.>emptyMap(); + } +} diff --git a/constructorio-client/src/main/java/io/constructor/client/models/SearchResponse.java b/constructorio-client/src/main/java/io/constructor/client/models/SearchResponse.java index d040717c..0e0355f7 100644 --- a/constructorio-client/src/main/java/io/constructor/client/models/SearchResponse.java +++ b/constructorio-client/src/main/java/io/constructor/client/models/SearchResponse.java @@ -4,7 +4,7 @@ import java.util.Map; /** Constructor.io Search Response ... uses Gson/Reflection to load data in */ -public class SearchResponse { +public class SearchResponse extends ResponseHeaders { @SerializedName("result_id") private String resultId; diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOAutocompleteUrlEncodingTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOAutocompleteUrlEncodingTest.java index 0c35c735..999d3614 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOAutocompleteUrlEncodingTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOAutocompleteUrlEncodingTest.java @@ -1,7 +1,11 @@ package io.constructor.client; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import io.constructor.client.models.AutocompleteResponse; +import java.util.List; +import java.util.Map; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -41,8 +45,7 @@ public void AutocompleteWithPlusShouldBeEncodedInUrl() throws Exception { constructor.autocomplete(request, null); RecordedRequest recordedRequest = mockServer.takeRequest(); - String expectedPath = - String.format("/autocomplete/r%%2Bco?key=%s&c=ciojava-7.5.0", apiKey); + String expectedPath = String.format("/autocomplete/r%%2Bco?key=%s&c=ciojava-7.5.0", apiKey); String actualPath = recordedRequest.getPath(); assertEquals("recorded request is encoded correctly", actualPath, expectedPath); } @@ -59,8 +62,7 @@ public void AutocompleteWithSpaceShouldBeEncodedInUrl() throws Exception { constructor.autocomplete(request, null); RecordedRequest recordedRequest = mockServer.takeRequest(); - String expectedPath = - String.format("/autocomplete/r%%20co?key=%s&c=ciojava-7.5.0", apiKey); + String expectedPath = String.format("/autocomplete/r%%20co?key=%s&c=ciojava-7.5.0", apiKey); String actualPath = recordedRequest.getPath(); assertEquals("recorded request is encoded correctly", actualPath, expectedPath); } @@ -77,8 +79,7 @@ public void AutocompleteWithSlashShouldBeEncodedInUrl() throws Exception { constructor.autocomplete(request, null); RecordedRequest recordedRequest = mockServer.takeRequest(); - String expectedPath = - String.format("/autocomplete/r%%2Fco?key=%s&c=ciojava-7.5.0", apiKey); + String expectedPath = String.format("/autocomplete/r%%2Fco?key=%s&c=ciojava-7.5.0", apiKey); String actualPath = recordedRequest.getPath(); assertEquals("recorded request is encoded correctly", actualPath, expectedPath); } @@ -99,4 +100,32 @@ public void AutocompleteWithSingleQuoteShouldBeAllowedInUrl() throws Exception { String actualPath = recordedRequest.getPath(); assertEquals("recorded request is encoded correctly", actualPath, expectedPath); } + + @Test + public void AutocompleteShouldReturnRateLimitHeaders() throws Exception { + String string = Utils.getTestResource("response.autocomplete.peanut.json"); + MockResponse mockResponse = + new MockResponse() + .setResponseCode(200) + .setBody(string) + .addHeader("X-RateLimit-Limit", "100") + .addHeader("X-RateLimit-Remaining", "99") + .addHeader("X-RateLimit-Reset", "1620000000"); + mockServer.enqueue(mockResponse); + + ConstructorIO constructor = + new ConstructorIO("", apiKey, false, "127.0.0.1", mockServer.getPort()); + AutocompleteRequest request = new AutocompleteRequest("peanut"); + AutocompleteResponse response = constructor.autocomplete(request, null); + + mockServer.takeRequest(); + + Map> headers = response.getHeaders(); + assertNotNull("headers should not be null", headers); + assertEquals("rate limit header", "100", headers.get("x-ratelimit-limit").get(0)); + assertEquals( + "rate limit remaining header", "99", headers.get("x-ratelimit-remaining").get(0)); + assertEquals( + "rate limit reset header", "1620000000", headers.get("x-ratelimit-reset").get(0)); + } } diff --git a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOSearchUrlEncodingTest.java b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOSearchUrlEncodingTest.java index 9c058e5c..9500f450 100644 --- a/constructorio-client/src/test/java/io/constructor/client/ConstructorIOSearchUrlEncodingTest.java +++ b/constructorio-client/src/test/java/io/constructor/client/ConstructorIOSearchUrlEncodingTest.java @@ -1,7 +1,11 @@ package io.constructor.client; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import io.constructor.client.models.SearchResponse; +import java.util.List; +import java.util.Map; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -108,4 +112,32 @@ public void SearchWithSingleQuoteShouldBeAllowedInUrl() throws Exception { String actualPath = recordedRequest.getPath(); assertEquals("recorded request is encoded correctly", actualPath, expectedPath); } + + @Test + public void SearchShouldReturnRateLimitHeaders() throws Exception { + String string = Utils.getTestResource("response.search.peanut.json"); + MockResponse mockResponse = + new MockResponse() + .setResponseCode(200) + .setBody(string) + .addHeader("X-RateLimit-Limit", "100") + .addHeader("X-RateLimit-Remaining", "99") + .addHeader("X-RateLimit-Reset", "1620000000"); + mockServer.enqueue(mockResponse); + + ConstructorIO constructor = + new ConstructorIO("", apiKey, false, "127.0.0.1", mockServer.getPort()); + SearchRequest request = new SearchRequest("peanut"); + SearchResponse response = constructor.search(request, null); + + mockServer.takeRequest(); + + Map> headers = response.getHeaders(); + assertNotNull("headers should not be null", headers); + assertEquals("rate limit header", "100", headers.get("x-ratelimit-limit").get(0)); + assertEquals( + "rate limit remaining header", "99", headers.get("x-ratelimit-remaining").get(0)); + assertEquals( + "rate limit reset header", "1620000000", headers.get("x-ratelimit-reset").get(0)); + } }