Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(java): add requestOptions #487

Merged
merged 5 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -99,20 +98,8 @@ private <T> 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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.algolia.utils;
Copy link
Member Author

@shortcuts shortcuts May 10, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


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<String, String> headers = new HashMap<String, String>();
private final Map<String, String> queryParams = new HashMap<String, String>();
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<String, String> getExtraHeaders() {
return headers;
}

public Map<String, String> 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 +
'\'' +
'}'
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,13 @@ export function createTransporter({
cacheable: baseRequestOptions?.cacheable,
timeout: baseRequestOptions?.timeout,
queryParameters: {
...baseRequestOptions?.queryParameters,
...methodOptions.queryParameters,
...baseRequestOptions?.queryParameters,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was wrong, parameters from requestOptions should always override defaults/methods parameters

},
headers: {
Accept: 'application/json',
...baseRequestOptions?.headers,
...methodOptions.headers,
...baseRequestOptions?.headers,
},
};

Expand Down
181 changes: 99 additions & 82 deletions templates/java/libraries/okhttp-gson/ApiClient.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,8 +28,7 @@ public class ApiClient {
private boolean debugging = false;
private Map<String, String> defaultHeaderMap = new HashMap<String, String>();


private String appId, apiKey;
private String contentType;

private DateFormat dateFormat;

Expand All @@ -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) {
Expand All @@ -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;
}

Expand Down Expand Up @@ -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.
*
Expand All @@ -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));
}

/**
Expand Down Expand Up @@ -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<String, String> queryParams, Object body, Map<String, String> headerParams) throws AlgoliaRuntimeException {
Request request = buildRequest(path, method, queryParams, body, headerParams);
public Call buildCall(String path, String method, Map<String, String> queryParams, Object body, Map<String, String> headerParams, RequestOptions requestOptions) throws AlgoliaRuntimeException {
Request request = buildRequest(path, method, queryParams, body, headerParams, requestOptions);

return requester.newCall(request);
}
Expand All @@ -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<String, String> queryParams, Object body, Map<String, String> 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<String, String> queryParams, Object body, Map<String, String> 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();
}
Expand All @@ -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<String, String> queryParams) {
final StringBuilder url = new StringBuilder();
public String buildUrl(String path, Map<String, String> queryParams, Map<String, String> 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<String, String> 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(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the logic was duplicated for both map I've used this, it seems to work but not a fan of the implem

String path,
StringBuilder url,
Map<String, String> queryParams
) {
if (queryParams != null && !queryParams.isEmpty()) {
// support (constant) query string in `path`, e.g. "/posts?draft=1"
String prefix = path.contains("?") ? "&" : "?";
for (Entry<String, String> param : queryParams.entrySet()) {
if (param.getValue() != null) {
if (prefix != null) {
url.append(prefix);
prefix = null;
} else {
url.append("&");
}
Comment on lines +364 to 369
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like...

Suggested change
if (prefix != null) {
url.append(prefix);
prefix = null;
} else {
url.append("&");
}
url.append(prefix);
if (prefix.equals("?")) {
prefix = "&";
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied the actual logic to avoid introducing unrelated changes, I don't really know the edge cases possible here D:

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<String, String> headerParams, Request.Builder reqBuilder) {
public void processHeaderParams(Map<String, String> headerParams, Map<String, String> extraHeaderParams, Request.Builder reqBuilder) {
for (Entry<String, String> param : headerParams.entrySet()) {
reqBuilder.header(param.getKey(), parameterToString(param.getValue()));
reqBuilder.header(param.getKey(), parameterToString(param.getValue()));
}
for (Entry<String, String> 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<String, String> header : extraHeaderParams.entrySet()) {
reqBuilder.header(
header.getKey(),
parameterToString(header.getValue())
);
}
}
}

Expand Down
Loading