From c5ac1854a6ce4f7be58e8bf924bbf6ecd2c14d89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Vannicatte?= Date: Wed, 11 May 2022 12:03:48 +0200 Subject: [PATCH] feat(java): add `requestOptions` (#487) --- .../java/com/algolia/utils/HttpRequester.java | 17 +- .../com/algolia/utils/RequestOptions.java | 62 ++++++ .../src/transporter/createTransporter.ts | 4 +- .../libraries/okhttp-gson/ApiClient.mustache | 181 ++++++++++-------- .../java/libraries/okhttp-gson/api.mustache | 45 ++++- 5 files changed, 205 insertions(+), 104 deletions(-) create mode 100644 clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/RequestOptions.java diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java index 5b60a89d1d..82c2cc9ffa 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/HttpRequester.java @@ -1,6 +1,5 @@ package com.algolia.utils; -import com.algolia.ApiClient; import com.algolia.exceptions.*; import com.algolia.utils.retry.RetryStrategy; import com.algolia.utils.retry.StatefulHost; @@ -99,20 +98,8 @@ private T deserialize(Response response, Type returnType) if (contentType == null) { contentType = "application/json"; } - if (ApiClient.isJsonMime(contentType)) { - return JSON.deserialize(respBody, returnType); - } else if (returnType.equals(String.class)) { - // Expecting string, return the raw response body. - return (T) respBody; - } else { - throw new AlgoliaApiException( - "Content type \"" + - contentType + - "\" is not supported for type: " + - returnType, - response.code() - ); - } + + return JSON.deserialize(respBody, returnType); } public void setDebugging(boolean debugging) { diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/RequestOptions.java b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/RequestOptions.java new file mode 100644 index 0000000000..b1306fca10 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/src/main/java/com/algolia/utils/RequestOptions.java @@ -0,0 +1,62 @@ +package com.algolia.utils; + +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nonnull; + +/** + * Request options are used to pass extra parameters, headers, timeout to the request. Parameters + * set in the request option will override default parameter. + */ +public class RequestOptions { + + private final Map headers = new HashMap(); + private final Map queryParams = new HashMap(); + private Integer timeout = null; + + public RequestOptions addExtraHeader( + @Nonnull String key, + @Nonnull String value + ) { + headers.put(key, value); + return this; + } + + public RequestOptions addExtraQueryParameters( + @Nonnull String key, + @Nonnull String value + ) { + queryParams.put(key, value); + return this; + } + + public Map getExtraHeaders() { + return headers; + } + + public Map getExtraQueryParams() { + return queryParams; + } + + public Integer getTimeout() { + return timeout; + } + + public RequestOptions setTimeout(Integer timeout) { + this.timeout = timeout; + return this; + } + + @Override + public String toString() { + return ( + "RequestOptions{" + + "headers=" + + headers + + ", queryParams=" + + queryParams + + '\'' + + '}' + ); + } +} diff --git a/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts b/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts index ce4a832332..fa9a44c48f 100644 --- a/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts +++ b/clients/algoliasearch-client-javascript/packages/client-common/src/transporter/createTransporter.ts @@ -240,13 +240,13 @@ export function createTransporter({ cacheable: baseRequestOptions?.cacheable, timeout: baseRequestOptions?.timeout, queryParameters: { - ...baseRequestOptions?.queryParameters, ...methodOptions.queryParameters, + ...baseRequestOptions?.queryParameters, }, headers: { Accept: 'application/json', - ...baseRequestOptions?.headers, ...methodOptions.headers, + ...baseRequestOptions?.headers, }, }; diff --git a/templates/java/libraries/okhttp-gson/ApiClient.mustache b/templates/java/libraries/okhttp-gson/ApiClient.mustache index c6b1efb11a..b0e3e16d97 100644 --- a/templates/java/libraries/okhttp-gson/ApiClient.mustache +++ b/templates/java/libraries/okhttp-gson/ApiClient.mustache @@ -4,6 +4,7 @@ import com.algolia.utils.Requester; import com.algolia.exceptions.*; import com.algolia.utils.UserAgent; import com.algolia.utils.JSON; +import com.algolia.utils.RequestOptions; import okhttp3.*; import okhttp3.internal.http.HttpMethod; @@ -27,8 +28,7 @@ public class ApiClient { private boolean debugging = false; private Map defaultHeaderMap = new HashMap(); - - private String appId, apiKey; + private String contentType; private DateFormat dateFormat; @@ -38,6 +38,8 @@ public class ApiClient { * Constructor for ApiClient with custom Requester */ public ApiClient(String appId, String apiKey, Requester requester, String clientName, UserAgent.Segment[] segments) { + this.contentType = "application/json"; + UserAgent ua = new UserAgent("{{packageVersion}}"); ua.addSegment(new UserAgent.Segment(clientName, "{{packageVersion}}")); if(segments != null) { @@ -47,8 +49,11 @@ public class ApiClient { } setUserAgent(ua.toString()); - this.appId = appId; - this.apiKey = apiKey; + defaultHeaderMap.put("X-Algolia-Application-Id", appId); + defaultHeaderMap.put("X-Algolia-API-Key", apiKey); + defaultHeaderMap.put("Accept", this.contentType); + defaultHeaderMap.put("Content-Type", this.contentType); + this.requester = requester; } @@ -189,22 +194,6 @@ public class ApiClient { } } - /** - * Check if the given MIME is a JSON MIME. - * JSON MIME examples: - * application/json - * application/json; charset=UTF8 - * APPLICATION/JSON - * application/vnd.company+json - * "* / *" is also default to JSON - * @param mime MIME (Multipurpose Internet Mail Extensions) - * @return True if the given MIME is JSON, false otherwise. - */ - public static boolean isJsonMime(String mime) { - String jsonMime = "(?i)^(application/json|[^;/ \t]+/[^;/ \t]+[+]json)[ \t]*(;.*)?$"; - return mime != null && (mime.matches(jsonMime) || mime.equals("*/*")); - } - /** * Escape the given string to be used as URL query value. * @@ -220,29 +209,23 @@ public class ApiClient { } /** - * Serialize the given Java object into request body according to the object's - * class and the request Content-Type. + * Serialize the given Java object into request body according to the object's class and the + * request Content-Type. * * @param obj The Java object - * @param contentType The request Content-Type * @return The serialized request body * @throws AlgoliaRuntimeException If fail to serialize the given object */ - public RequestBody serialize(Object obj, String contentType) throws AlgoliaRuntimeException { - if (obj instanceof byte[]) { - // Binary (byte array) body parameter support. - return RequestBody.create((byte[]) obj, MediaType.parse(contentType)); - } else if (isJsonMime(contentType)) { - String content; - if (obj != null) { - content = JSON.serialize(obj); - } else { - content = null; - } - return RequestBody.create(content, MediaType.parse(contentType)); - } else { - throw new AlgoliaRuntimeException("Content type \"" + contentType + "\" is not supported"); - } + public RequestBody serialize(Object obj) throws AlgoliaRuntimeException { + String content; + + if (obj != null) { + content = JSON.serialize(obj); + } else { + content = null; + } + + return RequestBody.create(content, MediaType.parse(this.contentType)); } /** @@ -286,11 +269,12 @@ public class ApiClient { * @param queryParams The query parameters * @param body The request body object * @param headerParams The header parameters + * @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. * @return The HTTP call * @throws AlgoliaRuntimeException If fail to serialize the request body object */ - public Call buildCall(String path, String method, Map queryParams, Object body, Map headerParams) throws AlgoliaRuntimeException { - Request request = buildRequest(path, method, queryParams, body, headerParams); + public Call buildCall(String path, String method, Map queryParams, Object body, Map headerParams, RequestOptions requestOptions) throws AlgoliaRuntimeException { + Request request = buildRequest(path, method, queryParams, body, headerParams, requestOptions); return requester.newCall(request); } @@ -303,37 +287,38 @@ public class ApiClient { * @param queryParams The query parameters * @param body The request body object * @param headerParams The header parameters + * @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. * @return The HTTP request * @throws AlgoliaRuntimeException If fail to serialize the request body object */ - public Request buildRequest(String path, String method, Map queryParams, Object body, Map headerParams) throws AlgoliaRuntimeException { - headerParams.put("X-Algolia-Application-Id", this.appId); - headerParams.put("X-Algolia-API-Key", this.apiKey); - headerParams.put("Accept", "application/json"); - headerParams.put("Content-Type", "application/json"); - - String contentType = "application/json"; - headerParams.put("Accept", contentType); - headerParams.put("Content-Type", contentType); - - final String url = buildUrl(path, queryParams); + public Request buildRequest(String path, String method, Map queryParams, Object body, Map headerParams, RequestOptions requestOptions) throws AlgoliaRuntimeException { + boolean hasRequestOptions = requestOptions != null; + final String url = buildUrl( + path, + queryParams, + hasRequestOptions ? requestOptions.getExtraQueryParams() : null + ); final Request.Builder reqBuilder = new Request.Builder().url(url); - processHeaderParams(headerParams, reqBuilder); - + processHeaderParams( + headerParams, + hasRequestOptions ? requestOptions.getExtraHeaders() : null, + reqBuilder + ); + RequestBody reqBody; if (!HttpMethod.permitsRequestBody(method)) { reqBody = null; } else if (body == null) { if ("DELETE".equals(method)) { - // allow calling DELETE without sending a request body - reqBody = null; + // allow calling DELETE without sending a request body + reqBody = null; } else { - // use an empty request body (for POST, PUT and PATCH) - reqBody = RequestBody.create("", MediaType.parse(contentType)); + // use an empty request body (for POST, PUT and PATCH) + reqBody = RequestBody.create("", MediaType.parse(this.contentType)); } - } else { - reqBody = serialize(body, contentType); - } + } else { + reqBody = serialize(body); + } return reqBuilder.method(method, reqBody).build(); } @@ -343,48 +328,80 @@ public class ApiClient { * * @param path The sub path * @param queryParams The query parameters + * @param extraQueryParams The query parameters, coming from the requestOptions * @return The full URL */ - public String buildUrl(String path, Map queryParams) { - final StringBuilder url = new StringBuilder(); + public String buildUrl(String path, Map queryParams, Map extraQueryParams) { + StringBuilder url = new StringBuilder(); //The real host will be assigned by the retry strategy url.append("http://temp.path").append(path); - if (queryParams != null && !queryParams.isEmpty()) { - // support (constant) query string in `path`, e.g. "/posts?draft=1" - String prefix = path.contains("?") ? "&" : "?"; - for (Entry param : queryParams.entrySet()) { - if (param.getValue() != null) { - if (prefix != null) { - url.append(prefix); - prefix = null; - } else { - url.append("&"); - } - String value = parameterToString(param.getValue()); - url.append(escapeString(param.getKey())).append("=").append(escapeString(value)); - } + url = parseQueryParameters(path, url, queryParams); + url = parseQueryParameters(path, url, extraQueryParams); + + return url.toString(); + } + + /** + * Parses the given map of Query Parameters to a given URL. + * + * @param path The sub path + * @param url The url to add queryParams to + * @param queryParams The query parameters + * @return The URL + */ + public StringBuilder parseQueryParameters( + String path, + StringBuilder url, + Map queryParams + ) { + if (queryParams != null && !queryParams.isEmpty()) { + // support (constant) query string in `path`, e.g. "/posts?draft=1" + String prefix = path.contains("?") ? "&" : "?"; + for (Entry param : queryParams.entrySet()) { + if (param.getValue() != null) { + if (prefix != null) { + url.append(prefix); + prefix = null; + } else { + url.append("&"); } + String value = parameterToString(param.getValue()); + url + .append(escapeString(param.getKey())) + .append("=") + .append(escapeString(value)); + } } + } - return url.toString(); + return url; } /** * Set header parameters to the request builder, including default headers. * * @param headerParams Header parameters in the form of Map + * @param extraHeaderParams Header parameters in the form of Map, coming from RequestOptions * @param reqBuilder Request.Builder */ - public void processHeaderParams(Map headerParams, Request.Builder reqBuilder) { + public void processHeaderParams(Map headerParams, Map extraHeaderParams, Request.Builder reqBuilder) { for (Entry param : headerParams.entrySet()) { - reqBuilder.header(param.getKey(), parameterToString(param.getValue())); + reqBuilder.header(param.getKey(), parameterToString(param.getValue())); } for (Entry header : defaultHeaderMap.entrySet()) { - if (!headerParams.containsKey(header.getKey())) { - reqBuilder.header(header.getKey(), parameterToString(header.getValue())); - } + if (!headerParams.containsKey(header.getKey())) { + reqBuilder.header(header.getKey(), parameterToString(header.getValue())); + } + } + if (extraHeaderParams != null) { + for (Entry header : extraHeaderParams.entrySet()) { + reqBuilder.header( + header.getKey(), + parameterToString(header.getValue()) + ); + } } } diff --git a/templates/java/libraries/okhttp-gson/api.mustache b/templates/java/libraries/okhttp-gson/api.mustache index 6510ed1f78..322392f4c6 100644 --- a/templates/java/libraries/okhttp-gson/api.mustache +++ b/templates/java/libraries/okhttp-gson/api.mustache @@ -12,6 +12,7 @@ import {{modelPackage}}.*; import com.algolia.exceptions.*; import com.algolia.utils.retry.CallType; import com.algolia.utils.retry.StatefulHost; +import com.algolia.utils.RequestOptions; import java.util.EnumSet; import java.util.Random; @@ -99,6 +100,7 @@ public class {{classname}} extends ApiClient { /** * {{¬es}}{{#allParams}} * @param {{paramName}} {{&description}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}{{/isContainer}}){{/required}}{{/allParams}}{{#returnType}} + * @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. * @return {{.}}{{/returnType}} * @throws AlgoliaRuntimeException If fail to call the API, e.g. server error or cannot deserialize the response body {{#isDeprecated}} @@ -112,13 +114,26 @@ public class {{classname}} extends ApiClient { {{#isDeprecated}} @Deprecated {{/isDeprecated}} - public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws AlgoliaRuntimeException { - return LaunderThrowable.await({{operationId}}Async({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}})); + public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}RequestOptions requestOptions) throws AlgoliaRuntimeException { + return LaunderThrowable.await({{operationId}}Async({{#allParams}}{{paramName}}, {{/allParams}}requestOptions)); } + {{! This case only sets `requestOptions` as optional }} + public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws AlgoliaRuntimeException { + {{#returnType}}return {{/returnType}}this.{{operationId}}({{#allParams}}{{paramName}}, {{/allParams}}null); + } + + {{! This case sets `requiredParams` + `requestOptions` }} + {{#optionalParams.0}} + public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#requiredParams}}{{{dataType}}} {{paramName}}, {{/requiredParams}}RequestOptions requestOptions) throws AlgoliaRuntimeException { + {{#returnType}}return {{/returnType}}this.{{operationId}}({{#requiredParams}}{{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#requiredParams.0}},{{/requiredParams.0}}{{#optionalParams}}null{{^-last}},{{/-last}}{{/optionalParams}}, requestOptions); + } + {{/optionalParams.0}} + + {{! This case only sets `requiredParams` }} {{#optionalParams.0}} public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#requiredParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}) throws AlgoliaRuntimeException { - {{#returnType}}return {{/returnType}}this.{{operationId}}({{#requiredParams}}{{paramName}}{{^-last}},{{/-last}}{{/requiredParams}}{{#requiredParams.0}},{{/requiredParams.0}}{{#optionalParams}}null{{^-last}},{{/-last}}{{/optionalParams}}); + {{#returnType}}return {{/returnType}}this.{{operationId}}({{#requiredParams}}{{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#requiredParams.0}},{{/requiredParams.0}}{{#optionalParams}}null{{^-last}},{{/-last}}{{/optionalParams}}, null); } {{/optionalParams.0}} @@ -126,6 +141,7 @@ public class {{classname}} extends ApiClient { * (asynchronously) * {{notes}}{{#allParams}} * @param {{paramName}} {{{description}}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}{{/isContainer}}){{/required}}{{/allParams}} + * @param requestOptions The requestOptions to send along with the query, they will be merged with the transporter requestOptions. * @return The awaitable future * @throws AlgoliaRuntimeException If fail to process the API call, e.g. serializing the request body object {{#isDeprecated}} @@ -139,7 +155,7 @@ public class {{classname}} extends ApiClient { {{#isDeprecated}} @Deprecated {{/isDeprecated}} - public CompletableFuture<{{{returnType}}}> {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}},{{/-last}} {{/allParams}}) throws AlgoliaRuntimeException { + public CompletableFuture<{{{returnType}}}> {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}RequestOptions requestOptions) throws AlgoliaRuntimeException { {{#allParams}}{{#required}} if ({{paramName}} == null) { throw new AlgoliaRuntimeException("Missing the required parameter '{{paramName}}' when calling {{operationId}}(Async)"); @@ -176,10 +192,29 @@ public class {{classname}} extends ApiClient { } {{/headerParams}} - Call call = this.buildCall(requestPath, "{{httpMethod}}", queryParams, bodyObj, headers); + Call call = this.buildCall(requestPath, "{{httpMethod}}", queryParams, bodyObj, headers, requestOptions); Type returnType = new TypeToken<{{{returnType}}}>() {}.getType(); return this.executeAsync(call, returnType); } + + {{! This case only sets `requestOptions` as optional }} + public CompletableFuture<{{{returnType}}}> {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws AlgoliaRuntimeException { + {{#returnType}}return {{/returnType}}this.{{operationId}}Async({{#allParams}}{{paramName}}, {{/allParams}}null); + } + + {{! This case sets `requiredParams` + `requestOptions` }} + {{#optionalParams.0}} + public CompletableFuture<{{{returnType}}}> {{operationId}}Async({{#requiredParams}}{{{dataType}}} {{paramName}}, {{/requiredParams}}RequestOptions requestOptions) throws AlgoliaRuntimeException { + {{#returnType}}return {{/returnType}}this.{{operationId}}Async({{#requiredParams}}{{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#requiredParams.0}},{{/requiredParams.0}}{{#optionalParams}}null{{^-last}},{{/-last}}{{/optionalParams}}, requestOptions); + } + {{/optionalParams.0}} + + {{! This case only sets `requiredParams` }} + {{#optionalParams.0}} + public CompletableFuture<{{{returnType}}}> {{operationId}}Async({{#requiredParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}) throws AlgoliaRuntimeException { + {{#returnType}}return {{/returnType}}this.{{operationId}}Async({{#requiredParams}}{{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}{{#requiredParams.0}},{{/requiredParams.0}}{{#optionalParams}}null{{^-last}},{{/-last}}{{/optionalParams}}, null); + } + {{/optionalParams.0}} {{/operation}} } {{/operations}}