diff --git a/clients/algoliasearch-client-java-2/.openapi-generator-ignore b/clients/algoliasearch-client-java-2/.openapi-generator-ignore index 7b4b7e7ffe..a90fa18640 100644 --- a/clients/algoliasearch-client-java-2/.openapi-generator-ignore +++ b/clients/algoliasearch-client-java-2/.openapi-generator-ignore @@ -19,6 +19,7 @@ gradle* # Selective source file algoliasearch-core/com/algolia/auth/** +algoliasearch-core/com/algolia/ApiException.java algoliasearch-core/com/algolia/Configuration.java algoliasearch-core/com/algolia/Server*.java algoliasearch-core/com/algolia/StringUtil.java diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/ApiException.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/ApiException.java deleted file mode 100644 index 06c74f5cc5..0000000000 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/ApiException.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.algolia; - -import java.util.List; -import java.util.Map; - -public class ApiException extends Exception { - - private int code = 0; - private Map> responseHeaders = null; - private String responseBody = null; - - public ApiException() {} - - public ApiException(Throwable throwable) { - super(throwable); - } - - public ApiException(String message) { - super(message); - } - - public ApiException( - String message, - Throwable throwable, - int code, - Map> responseHeaders, - String responseBody - ) { - super(message, throwable); - this.code = code; - this.responseHeaders = responseHeaders; - this.responseBody = responseBody; - } - - public ApiException( - String message, - int code, - Map> responseHeaders, - String responseBody - ) { - this(message, (Throwable) null, code, responseHeaders, responseBody); - } - - public ApiException( - String message, - Throwable throwable, - int code, - Map> responseHeaders - ) { - this(message, throwable, code, responseHeaders, null); - } - - public ApiException( - int code, - Map> responseHeaders, - String responseBody - ) { - this((String) null, (Throwable) null, code, responseHeaders, responseBody); - } - - public ApiException(int code, String message) { - super(message); - this.code = code; - } - - public ApiException( - int code, - String message, - Map> responseHeaders, - String responseBody - ) { - this(code, message); - this.responseHeaders = responseHeaders; - this.responseBody = responseBody; - } - - /** - * Get the HTTP status code. - * - * @return HTTP status code - */ - public int getCode() { - return code; - } - - /** - * Get the HTTP response headers. - * - * @return A map of list of string - */ - public Map> getResponseHeaders() { - return responseHeaders; - } - - /** - * Get the HTTP response body. - * - * @return Response body in the form of string - */ - public String getResponseBody() { - return responseBody; - } -} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaApiException.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaApiException.java new file mode 100644 index 0000000000..40ca6e1835 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaApiException.java @@ -0,0 +1,30 @@ +package com.algolia.exceptions; + +/** Exception thrown in case of API failure such as 4XX, 5XX error. */ +public class AlgoliaApiException extends AlgoliaRuntimeException { + + public int getHttpErrorCode() { + return httpErrorCode; + } + + private final int httpErrorCode; + + public AlgoliaApiException( + String message, + Throwable cause, + int httpErrorCode + ) { + super(message, cause); + this.httpErrorCode = httpErrorCode; + } + + public AlgoliaApiException(String message, int httpErrorCode) { + super(message); + this.httpErrorCode = httpErrorCode; + } + + public AlgoliaApiException(Throwable cause, int httpErrorCode) { + super(cause); + this.httpErrorCode = httpErrorCode; + } +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaRetryException.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaRetryException.java new file mode 100644 index 0000000000..bb0668b6e2 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaRetryException.java @@ -0,0 +1,20 @@ +package com.algolia.exceptions; + +/** + * Exception thrown when an error occurs during the retry strategy. For example: All hosts are + * unreachable. + */ +public class AlgoliaRetryException extends AlgoliaRuntimeException { + + public AlgoliaRetryException(String message, Throwable cause) { + super(message, cause); + } + + public AlgoliaRetryException(String message) { + super(message); + } + + public AlgoliaRetryException(Throwable cause) { + super(cause); + } +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaRuntimeException.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaRuntimeException.java new file mode 100644 index 0000000000..6ba51537d8 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/exceptions/AlgoliaRuntimeException.java @@ -0,0 +1,17 @@ +package com.algolia.exceptions; + +/** Exception thrown when an error occurs during the Serialization/Deserialization process */ +public class AlgoliaRuntimeException extends RuntimeException { + + public AlgoliaRuntimeException(String message, Throwable cause) { + super(message, cause); + } + + public AlgoliaRuntimeException(String message) { + super(message); + } + + public AlgoliaRuntimeException(Throwable cause) { + super(cause); + } +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/HttpRequester.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/HttpRequester.java index 1da39b461e..07cbae53b4 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/HttpRequester.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/HttpRequester.java @@ -1,9 +1,11 @@ package com.algolia.utils; import com.algolia.ApiCallback; -import com.algolia.ApiException; import com.algolia.ProgressResponseBody; +import com.algolia.utils.retry.RetryStrategy; +import com.algolia.utils.retry.StatefulHost; import java.io.IOException; +import java.util.List; import java.util.concurrent.TimeUnit; import okhttp3.Call; import okhttp3.Interceptor; @@ -15,18 +17,23 @@ public class HttpRequester implements Requester { + private RetryStrategy retryStrategy; private OkHttpClient httpClient; private HttpLoggingInterceptor loggingInterceptor; private boolean debugging; - public HttpRequester() { + public HttpRequester(List hosts) { + this.retryStrategy = new RetryStrategy(hosts); + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + builder.addInterceptor(retryStrategy.getRetryInterceptor()); builder.addNetworkInterceptor(getProgressInterceptor()); + builder.retryOnConnectionFailure(false); httpClient = builder.build(); } - public Call newCall(Request request) throws ApiException { + public Call newCall(Request request) { return httpClient.newCall(request); } diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/Requester.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/Requester.java index 9f7f262846..919dd7c8bb 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/Requester.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/Requester.java @@ -1,11 +1,10 @@ package com.algolia.utils; -import com.algolia.ApiException; import okhttp3.Call; import okhttp3.Request; public interface Requester { - public Call newCall(Request request) throws ApiException; + public Call newCall(Request request); /** * Enable/disable debugging for this API client. diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/Utils.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/Utils.java new file mode 100644 index 0000000000..9c5e7cfd89 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/Utils.java @@ -0,0 +1,21 @@ +package com.algolia.utils; + +import java.time.Clock; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.zone.ZoneRules; + +public class Utils { + + private static final ZoneRules ZONE_RULES_UTC = ZoneOffset.UTC.getRules(); + + /** + * Memory optimization for getZoneRules with the same ZoneOffset (UTC). ZoneRules is immutable and + * threadsafe, but getRules method consumes a lot of memory during load testing. + */ + public static OffsetDateTime nowUTC() { + final Instant now = Clock.system(ZoneOffset.UTC).instant(); + return OffsetDateTime.ofInstant(now, ZONE_RULES_UTC.getOffset(now)); + } +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/echo/EchoRequester.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/echo/EchoRequester.java index 7719400fcd..8e49cf5b13 100644 --- a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/echo/EchoRequester.java +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/echo/EchoRequester.java @@ -1,34 +1,46 @@ package com.algolia.utils.echo; -import com.algolia.ApiException; import com.algolia.utils.Requester; import okhttp3.Request; public class EchoRequester implements Requester { - public CallEcho newCall(Request request) throws ApiException { + private int connectionTimeout, readTimeout, writeTimeout; + + public EchoRequester() { + this.connectionTimeout = 100; + this.readTimeout = 100; + this.writeTimeout = 100; + } + + public CallEcho newCall(Request request) { return new CallEcho(request); } // NO-OP for now - public void setDebugging(boolean debugging) {} public int getConnectTimeout() { - return 100; + return this.connectionTimeout; } - public void setConnectTimeout(int connectionTimeout) {} + public void setConnectTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } public int getReadTimeout() { - return 100; + return this.readTimeout; } - public void setReadTimeout(int readTimeout) {} + public void setReadTimeout(int readTimeout) { + this.readTimeout = readTimeout; + } public int getWriteTimeout() { - return 100; + return this.writeTimeout; } - public void setWriteTimeout(int writeTimeout) {} + public void setWriteTimeout(int writeTimeout) { + this.writeTimeout = writeTimeout; + } } diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/CallType.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/CallType.java new file mode 100644 index 0000000000..20614ba962 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/CallType.java @@ -0,0 +1,6 @@ +package com.algolia.utils.retry; + +public enum CallType { + READ, + WRITE, +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/RetryOutcome.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/RetryOutcome.java new file mode 100644 index 0000000000..f4c2c8b726 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/RetryOutcome.java @@ -0,0 +1,7 @@ +package com.algolia.utils.retry; + +public enum RetryOutcome { + SUCCESS, + RETRY, + FAILURE, +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/RetryStrategy.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/RetryStrategy.java new file mode 100644 index 0000000000..200a4cdce8 --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/RetryStrategy.java @@ -0,0 +1,143 @@ +package com.algolia.utils.retry; + +import com.algolia.exceptions.*; +import com.algolia.utils.Utils; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.net.UnknownHostException; +import java.time.Duration; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import okhttp3.HttpUrl; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +public class RetryStrategy { + + private final List hosts; + + public RetryStrategy(List hosts) { + this.hosts = hosts; + } + + public Interceptor getRetryInterceptor() { + return new Interceptor() { + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + Iterator hostsIter = getTryableHosts( + request.method().equals("GET") ? CallType.READ : CallType.WRITE) + .iterator(); + while (hostsIter.hasNext()) { + StatefulHost currentHost = hostsIter.next(); + + // Building the request URL + HttpUrl newUrl = request + .url() + .newBuilder() + .scheme(currentHost.getScheme()) + .host(currentHost.getHost()) + .build(); + request = request.newBuilder().url(newUrl).build(); + + // Computing timeout with the retry count + chain.withConnectTimeout( + chain.connectTimeoutMillis() + currentHost.getRetryCount() * 1000, + TimeUnit.MILLISECONDS); + + try { + System.out.println("MAKING REQUEST TO " + newUrl + " try: " + currentHost.getRetryCount()); + Response response = chain.proceed(request); + currentHost.setLastUse(Utils.nowUTC()); + // no timeout + if (response.isSuccessful()) { + currentHost.setUp(true); + return response; + } + if (isRetryable(response)) { + currentHost.setUp(false); + response.close(); + continue; + } + // unkown state, fail + throw new AlgoliaApiException(response.message(), response.code()); + } catch (AlgoliaApiException e) { + throw e; + } catch (SocketTimeoutException e) { + // timeout + currentHost.setUp(true); + currentHost.setLastUse(Utils.nowUTC()); + currentHost.incrementRetryCount(); + } catch (UnknownHostException e) { + throw new AlgoliaApiException(e.getMessage(), 404); + } catch (Exception e) { + throw new AlgoliaApiException(e.getMessage(), 400); + } + } + throw new AlgoliaRetryException("All hosts are unreachable"); + } + }; + + } + + /** + * Tells if the response is retryable or not depending on the http status code + * + * @param response Algolia's API response + */ + private boolean isRetryable(Response response) { + return response.code() / 100 != 2 && response.code() / 100 != 4; + } + + /** + * Gives the available hosts. + * + * @param callType Algolia calltype. + */ + List getTryableHosts(CallType callType) { + synchronized (this) { + resetExpiredHosts(); + if (hosts + .stream() + .anyMatch(h -> h.isUp() && h.getAccept().contains(callType))) { + return hosts + .stream() + .filter(h -> h.isUp() && h.getAccept().contains(callType)) + .collect(Collectors.toList()); + } else { + for (StatefulHost host : hosts + .stream() + .filter(h -> h.getAccept().contains(callType)) + .collect(Collectors.toList())) { + reset(host); + } + + return hosts; + } + } + } + + /** + * Reset the given hosts. Sets the retry count to 0 and set the last use to now. + * + * @param host The host to reset + */ + private void reset(StatefulHost host) { + host.setUp(true).setRetryCount(0).setLastUse(Utils.nowUTC()); + } + + /** Reset all hosts down for more than 5 minutes. */ + private void resetExpiredHosts() { + for (StatefulHost host : hosts) { + long lastUse = Duration + .between(Utils.nowUTC(), host.getLastUse()) + .getSeconds(); + if (!host.isUp() && Math.abs(lastUse) > 5 * 60) { + reset(host); + } + } + } +} diff --git a/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/StatefulHost.java b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/StatefulHost.java new file mode 100644 index 0000000000..b79b1e781b --- /dev/null +++ b/clients/algoliasearch-client-java-2/algoliasearch-core/com/algolia/utils/retry/StatefulHost.java @@ -0,0 +1,78 @@ +package com.algolia.utils.retry; + +import com.algolia.utils.Utils; +import java.time.OffsetDateTime; +import java.util.EnumSet; + +public class StatefulHost { + + private String host, scheme; + private boolean up = true; + private int retryCount; + private OffsetDateTime lastUse = Utils.nowUTC(); + private EnumSet accept; + + public StatefulHost(String host, String scheme, EnumSet accept) { + this.host = host; + this.scheme = scheme; + this.accept = accept; + } + + public String getHost() { + return host; + } + + StatefulHost setHost(String host) { + this.host = host; + return this; + } + + public String getScheme() { + return this.scheme; + } + + StatefulHost setScheme(String scheme) { + this.scheme = scheme; + return this; + } + + public boolean isUp() { + return up; + } + + StatefulHost setUp(boolean up) { + this.up = up; + return this; + } + + public int getRetryCount() { + return retryCount; + } + + StatefulHost setRetryCount(int retryCount) { + this.retryCount = retryCount; + return this; + } + + void incrementRetryCount() { + this.retryCount++; + } + + public OffsetDateTime getLastUse() { + return lastUse; + } + + StatefulHost setLastUse(OffsetDateTime lastUse) { + this.lastUse = lastUse; + return this; + } + + public EnumSet getAccept() { + return accept; + } + + StatefulHost setAccept(EnumSet accept) { + this.accept = accept; + return this; + } +} diff --git a/generators/src/main/java/com/algolia/codegen/AlgoliaJavaGenerator.java b/generators/src/main/java/com/algolia/codegen/AlgoliaJavaGenerator.java index 09a8dac8b5..2b1c5b4442 100644 --- a/generators/src/main/java/com/algolia/codegen/AlgoliaJavaGenerator.java +++ b/generators/src/main/java/com/algolia/codegen/AlgoliaJavaGenerator.java @@ -160,8 +160,9 @@ public String getHelp() { return "Generates an algolia-java client library."; } - public AlgoliaJavaGenerator() { - super(); + @Override + public void processOpts() { + super.processOpts(); supportingFiles.add(new SupportingFile("EchoResponse.mustache", "algoliasearch-core/com/algolia/utils/echo", diff --git a/playground/java/src/main/java/com/algolia/playground/App.java b/playground/java/src/main/java/com/algolia/playground/App.java index 071830147f..104e9ba72e 100644 --- a/playground/java/src/main/java/com/algolia/playground/App.java +++ b/playground/java/src/main/java/com/algolia/playground/App.java @@ -1,30 +1,42 @@ package com.algolia.playground; +import com.algolia.exceptions.AlgoliaApiException; +import com.algolia.exceptions.AlgoliaRetryException; +import com.algolia.exceptions.AlgoliaRuntimeException; import com.algolia.model.search.*; import com.algolia.search.SearchApi; -import com.algolia.ApiException; -import com.algolia.utils.echo.*; import io.github.cdimascio.dotenv.Dotenv; public class App { - public static void main(String[] args) { - Dotenv dotenv = Dotenv.configure().directory("../").load(); - - SearchApi client = new SearchApi(dotenv.get("ALGOLIA_APPLICATION_ID"), dotenv.get("ALGOLIA_SEARCH_KEY")); - String indexName = dotenv.get("SEARCH_INDEX"); - SearchParamsObject params = new SearchParamsObject(); - params.setAroundRadius(AroundRadius.of(5)); - params.setQuery(dotenv.get("SEARCH_QUERY")); - try { - SearchResponse result = client.search(indexName, SearchParams.of(params)); - System.out.println(result); - } catch (ApiException e) { - System.err.println("Exception when calling SearchApi#search"); - System.err.println("Status code: " + e.getCode()); - System.err.println("Reason: " + e.getResponseBody()); - System.err.println("Response headers: " + e.getResponseHeaders()); - e.printStackTrace(); - } + public static void main(String[] args) { + Dotenv dotenv = Dotenv.configure().directory("../").load(); + + SearchApi client = new SearchApi(dotenv.get("ALGOLIA_APPLICATION_ID"), dotenv.get("ALGOLIA_SEARCH_KEY")); + + String indexName = dotenv.get("SEARCH_INDEX"); + SearchParamsObject params = new SearchParamsObject(); + params.setAroundRadius(AroundRadius.ofInteger(5)); + params.setQuery(dotenv.get("SEARCH_QUERY")); + + try { + SearchResponse result = client.search(indexName, SearchParams.ofSearchParamsObject(params)); + System.out.println(result); + } catch (AlgoliaApiException e) { + // the API failed + System.err.println("Exception when calling SearchApi#search"); + System.err.println("Status code: " + e.getHttpErrorCode()); + System.err.println("Reason: " + e.getMessage()); + e.printStackTrace(); + + } catch (AlgoliaRetryException e) { + // the retry failed + System.err.println("Exception in the retry strategy"); + e.printStackTrace(); + + } catch (AlgoliaRuntimeException e) { + // the serialization or something else failed + e.printStackTrace(); } + } } diff --git a/templates/java/libraries/okhttp-gson/ApiCallback.mustache b/templates/java/libraries/okhttp-gson/ApiCallback.mustache index b4889a0319..abaab5a917 100644 --- a/templates/java/libraries/okhttp-gson/ApiCallback.mustache +++ b/templates/java/libraries/okhttp-gson/ApiCallback.mustache @@ -3,6 +3,8 @@ package {{invokerPackage}}; import java.util.Map; import java.util.List; +import com.algolia.exceptions.AlgoliaRuntimeException; + /** * Callback for asynchronous API call. * @@ -16,7 +18,7 @@ public interface ApiCallback { * @param statusCode Status code of the response if available, otherwise it would be 0 * @param responseHeaders Headers of the response if available, otherwise it would be null */ - void onFailure(ApiException e, int statusCode, Map> responseHeaders); + void onFailure(AlgoliaRuntimeException e, int statusCode, Map> responseHeaders); /** * This is called when the API call succeeded. diff --git a/templates/java/libraries/okhttp-gson/ApiClient.mustache b/templates/java/libraries/okhttp-gson/ApiClient.mustache index 261530c058..7fdd66508a 100644 --- a/templates/java/libraries/okhttp-gson/ApiClient.mustache +++ b/templates/java/libraries/okhttp-gson/ApiClient.mustache @@ -1,6 +1,7 @@ package {{invokerPackage}}; import com.algolia.utils.Requester; +import com.algolia.exceptions.*; import okhttp3.*; import okhttp3.internal.http.HttpMethod; @@ -26,8 +27,7 @@ public class ApiClient { private boolean debugging = false; private Map defaultHeaderMap = new HashMap(); - - private String basePath; + private String appId, apiKey; private DateFormat dateFormat; @@ -40,7 +40,6 @@ public class ApiClient { public ApiClient(String appId, String apiKey, Requester requester) { setUserAgent("{{{httpUserAgent}}}{{^httpUserAgent}}OpenAPI-Generator/{{{artifactVersion}}}/java{{/httpUserAgent}}"); - this.basePath = "https://" + appId + "-1.algolianet.com"; this.appId = appId; this.apiKey = apiKey; this.requester = requester; @@ -329,10 +328,10 @@ public class ApiClient { * @param response HTTP response * @param returnType The type of the Java object * @return The deserialized Java object - * @throws ApiException If fail to deserialize response body, i.e. cannot read response body + * @throws AlgoliaRuntimeException If fail to deserialize response body, i.e. cannot read response body * or the Content-Type of the response is not supported. */ - public T deserialize(Response response, Type returnType) throws ApiException { + public T deserialize(Response response, Type returnType) throws AlgoliaRuntimeException { if (response == null || returnType == null) { return null; } @@ -342,7 +341,7 @@ public class ApiClient { try { return (T) response.body().bytes(); } catch (IOException e) { - throw new ApiException(e); + throw new AlgoliaRuntimeException(e); } } @@ -353,7 +352,7 @@ public class ApiClient { else respBody = null; } catch (IOException e) { - throw new ApiException(e); + throw new AlgoliaRuntimeException(e); } if (respBody == null || "".equals(respBody)) { @@ -371,11 +370,9 @@ public class ApiClient { // Expecting string, return the raw response body. return (T) respBody; } else { - throw new ApiException( + throw new AlgoliaApiException( "Content type \"" + contentType + "\" is not supported for type: " + returnType, - response.code(), - response.headers().toMultimap(), - respBody); + response.code()); } } @@ -386,9 +383,9 @@ public class ApiClient { * @param obj The Java object * @param contentType The request Content-Type * @return The serialized request body - * @throws ApiException If fail to serialize the given object + * @throws AlgoliaRuntimeException If fail to serialize the given object */ - public RequestBody serialize(Object obj, String contentType) throws ApiException { + 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)); @@ -401,7 +398,7 @@ public class ApiClient { } return RequestBody.create(content, MediaType.parse(contentType)); } else { - throw new ApiException("Content type \"" + contentType + "\" is not supported"); + throw new AlgoliaRuntimeException("Content type \"" + contentType + "\" is not supported"); } } @@ -411,9 +408,9 @@ public class ApiClient { * @param Type * @param call An instance of the Call object * @return ApiResponse<T> - * @throws ApiException If fail to execute the call + * @throws AlgoliaRuntimeException If fail to execute the call */ - public ApiResponse execute(Call call) throws ApiException { + public ApiResponse execute(Call call) throws AlgoliaRuntimeException { return execute(call, null); } @@ -426,35 +423,18 @@ public class ApiClient { * @return ApiResponse object containing response status, headers and * data, which is a Java object deserialized from response body and would be null * when returnType is null. - * @throws ApiException If fail to execute the call + * @throws AlgoliaRuntimeException If fail to execute the call */ - public ApiResponse execute(Call call, Type returnType) throws ApiException { + public ApiResponse execute(Call call, Type returnType) throws AlgoliaRuntimeException { try { Response response = call.execute(); T data = handleResponse(response, returnType); return new ApiResponse(response.code(), response.headers().toMultimap(), data); } catch (IOException e) { - throw new ApiException(e); + throw new AlgoliaRuntimeException(e); } } - {{#supportStreaming}} - public InputStream executeStream(Call call, Type returnType) throws ApiException { - try { - Response response = call.execute(); - if (!response.isSuccessful()) { - throw new ApiException(response.code(), response.message()); - } - if (response.body() == null) { - return null; - } - return response.body().byteStream(); - } catch (IOException e) { - throw new ApiException(e); - } - } - - {{/supportStreaming}} /** * {@link #executeAsync(Call, Type, ApiCallback)} * @@ -479,7 +459,7 @@ public class ApiClient { call.enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { - callback.onFailure(new ApiException(e), 0, null); + callback.onFailure(new AlgoliaRuntimeException(e), 0, null); } @Override @@ -487,11 +467,11 @@ public class ApiClient { T result; try { result = (T) handleResponse(response, returnType); - } catch (ApiException e) { + } catch (AlgoliaRuntimeException e) { callback.onFailure(e, response.code(), response.headers().toMultimap()); return; } catch (Exception e) { - callback.onFailure(new ApiException(e), response.code(), response.headers().toMultimap()); + callback.onFailure(new AlgoliaRuntimeException(e), response.code(), response.headers().toMultimap()); return; } callback.onSuccess(result, response.code(), response.headers().toMultimap()); @@ -506,10 +486,10 @@ public class ApiClient { * @param response Response * @param returnType Return type * @return Type - * @throws ApiException If the response has an unsuccessful status code or + * @throws AlgoliaRuntimeException If the response has an unsuccessful status code or * fail to deserialize the response body */ - public T handleResponse(Response response, Type returnType) throws ApiException { + public T handleResponse(Response response, Type returnType) throws AlgoliaRuntimeException { if (response.isSuccessful()) { if (returnType == null || response.code() == 204) { // returning null if the returnType is not defined, @@ -518,7 +498,7 @@ public class ApiClient { try { response.body().close(); } catch (Exception e) { - throw new ApiException(response.message(), e, response.code(), response.headers().toMultimap()); + throw new AlgoliaApiException(response.message(), e, response.code()); } } return null; @@ -526,15 +506,14 @@ public class ApiClient { return deserialize(response, returnType); } } else { - String respBody = null; if (response.body() != null) { try { - respBody = response.body().string(); + response.body().string(); } catch (IOException e) { - throw new ApiException(response.message(), e, response.code(), response.headers().toMultimap()); + throw new AlgoliaApiException(response.message(), e, response.code()); } } - throw new ApiException(response.message(), response.code(), response.headers().toMultimap(), respBody); + throw new AlgoliaApiException(response.message(), response.code()); } } @@ -548,9 +527,9 @@ public class ApiClient { * @param headerParams The header parameters * @param callback Callback for upload/download progress * @return The HTTP call - * @throws ApiException If fail to serialize the request body object + * @throws AlgoliaRuntimeException If fail to serialize the request body object */ - public Call buildCall(String path, String method, List queryParams, Object body, Map headerParams, ApiCallback callback) throws ApiException { + public Call buildCall(String path, String method, List queryParams, Object body, Map headerParams, ApiCallback callback) throws AlgoliaRuntimeException { Request request = buildRequest(path, method, queryParams, body, headerParams, callback); return requester.newCall(request); @@ -566,9 +545,9 @@ public class ApiClient { * @param headerParams The header parameters * @param callback Callback for upload/download progress * @return The HTTP request - * @throws ApiException If fail to serialize the request body object + * @throws AlgoliaRuntimeException If fail to serialize the request body object */ - public Request buildRequest(String path, String method, List queryParams, Object body, Map headerParams, ApiCallback callback) throws ApiException { + public Request buildRequest(String path, String method, List queryParams, Object body, Map headerParams, ApiCallback callback) throws AlgoliaRuntimeException { headerParams.put("X-Algolia-Application-Id", this.appId); headerParams.put("X-Algolia-API-Key", this.apiKey); @@ -622,7 +601,9 @@ public class ApiClient { */ public String buildUrl(String path, List queryParams) { final StringBuilder url = new StringBuilder(); - url.append(basePath).append(path); + + //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" diff --git a/templates/java/libraries/okhttp-gson/api.mustache b/templates/java/libraries/okhttp-gson/api.mustache index f288023c54..77695f0282 100644 --- a/templates/java/libraries/okhttp-gson/api.mustache +++ b/templates/java/libraries/okhttp-gson/api.mustache @@ -2,7 +2,6 @@ package {{package}}; import {{invokerPackage}}.ApiCallback; import {{invokerPackage}}.ApiClient; -import {{invokerPackage}}.ApiException; import {{invokerPackage}}.ApiResponse; import {{invokerPackage}}.Pair; @@ -14,7 +13,13 @@ import okhttp3.Request; import com.algolia.utils.*; import com.algolia.utils.echo.*; import com.algolia.model.search.*; +import com.algolia.exceptions.*; +import com.algolia.utils.retry.CallType; +import com.algolia.utils.retry.StatefulHost; +import java.util.EnumSet; +import java.util.Random; +import java.util.Collections; import java.lang.reflect.Type; {{^fullJavaUtil}} import java.util.ArrayList; @@ -22,23 +27,75 @@ import java.util.HashMap; import java.util.List; import java.util.Map; {{/fullJavaUtil}} +import java.util.stream.Collectors; +import java.util.stream.Stream; {{#operations}} public class {{classname}} extends ApiClient { + {{#hasRegionalHost}} + {{#fallbackToAliasHost}} public {{classname}}(String appId, String apiKey) { - super(appId, apiKey, new HttpRequester()); + super(appId, apiKey, new HttpRequester(getDefaultHosts("."))); } + + {{/fallbackToAliasHost}} + public {{classname}}(String appId, String apiKey, String region) { + super(appId, apiKey, new HttpRequester(getDefaultHosts(region))); + } + {{/hasRegionalHost}} + {{^hasRegionalHost}} + public {{classname}}(String appId, String apiKey) { + super(appId, apiKey, new HttpRequester(getDefaultHosts(appId))); + } + {{/hasRegionalHost}} + public {{classname}}(String appId, String apiKey, Requester requester) { super(appId, apiKey, requester); } + {{^hasRegionalHost}}{{^experimentalHost}} + private static List getDefaultHosts(String appId) { + List hosts = new ArrayList(); + hosts.add(new StatefulHost(appId + "-dsn.algolia.net", "https", EnumSet.of(CallType.READ))); + hosts.add(new StatefulHost(appId + ".algolia.net", "https", EnumSet.of(CallType.WRITE))); + + List commonHosts = new ArrayList(); + hosts.add(new StatefulHost(appId + "-1.algolianet.net", "https", EnumSet.of(CallType.READ, CallType.WRITE))); + hosts.add(new StatefulHost(appId + "-2.algolianet.net", "https", EnumSet.of(CallType.READ, CallType.WRITE))); + hosts.add(new StatefulHost(appId + "-3.algolianet.net", "https", EnumSet.of(CallType.READ, CallType.WRITE))); + + Collections.shuffle(commonHosts, new Random()); + + return Stream.concat(hosts.stream(), commonHosts.stream()).collect(Collectors.toList()); + } + {{/experimentalHost}}{{/hasRegionalHost}} + + {{#experimentalHost}} + private static List getDefaultHosts() { + List hosts = new ArrayList(); + hosts.add(new StatefulHost("https", "{{{experimentalHost}}}", EnumSet.of(CallType.READ, CallType.WRITE))); + return hosts; + } + {{/experimentalHost}} + + {{#hasRegionalHost}} + private static List getDefaultHosts(String region) { + List hosts = new ArrayList(); + hosts.add(new StatefulHost({{{host}}} + "." + region + ".algolia.{{#topLevelDomain}}{{.}}{{/topLevelDomain}}{{^topLevelDomain}}com{{/topLevelDomain}}", "https", EnumSet.of(CallType.READ, CallType.WRITE))); + + hosts.add(new StatefulHost(appId + "-1.algolianet.com", "https", EnumSet.of(CallType.READ, CallType.WRITE))); + + return hosts; + } + {{/hasRegionalHost}} + {{#operation}} /** * Build call for {{operationId}} * @param _callback Callback for upload/download progress * @return Call to execute - * @throws ApiException If fail to serialize the request body object + * @throws AlgoliaRuntimeException If fail to serialize the request body object {{#isDeprecated}} * @deprecated {{/isDeprecated}} @@ -50,7 +107,7 @@ public class {{classname}} extends ApiClient { {{#isDeprecated}} @Deprecated {{/isDeprecated}} - private Call {{operationId}}Call({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ApiCallback<{{{returnType}}}> _callback) throws ApiException { + private Call {{operationId}}Call({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ApiCallback<{{{returnType}}}> _callback) throws AlgoliaRuntimeException { Object bodyObj = {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}}; // create path and map variables @@ -81,11 +138,11 @@ public class {{classname}} extends ApiClient { {{#isDeprecated}} @Deprecated {{/isDeprecated}} - private Call {{operationId}}ValidateBeforeCall({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ApiCallback<{{{returnType}}}> _callback) throws ApiException { + private Call {{operationId}}ValidateBeforeCall({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ApiCallback<{{{returnType}}}> _callback) throws AlgoliaRuntimeException { {{#allParams}}{{#required}} // verify the required parameter '{{paramName}}' is set if ({{paramName}} == null) { - throw new ApiException("Missing the required parameter '{{paramName}}' when calling {{operationId}}(Async)"); + throw new AlgoliaRuntimeException("Missing the required parameter '{{paramName}}' when calling {{operationId}}(Async)"); } {{/required}}{{/allParams}} @@ -96,7 +153,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}} * @return {{.}}{{/returnType}} - * @throws ApiException If fail to call the API, e.g. server error or cannot deserialize the response body + * @throws AlgoliaRuntimeException If fail to call the API, e.g. server error or cannot deserialize the response body {{#isDeprecated}} * @deprecated {{/isDeprecated}} @@ -108,7 +165,7 @@ public class {{classname}} extends ApiClient { {{#isDeprecated}} @Deprecated {{/isDeprecated}} - public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { + public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws AlgoliaRuntimeException { Call req = {{operationId}}ValidateBeforeCall({{#allParams}}{{paramName}}, {{/allParams}}null); if (req instanceof CallEcho) { {{#returnType}}return new EchoResponse.{{baseName}}Echo.{{#lambda.titlecase}}{{{operationId}}}{{/lambda.titlecase}}(((CallEcho)req).request());{{/returnType}} @@ -120,7 +177,7 @@ public class {{classname}} extends ApiClient { } {{#optionalParams.0}} - public {{#returnType}}{{{.}}} {{/returnType}}{{^returnType}}void {{/returnType}}{{operationId}}({{#requiredParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/requiredParams}}) throws ApiException { + 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}}{{#schema}}{{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}}{{/schema}}{{^schema}}null{{/schema}}{{^-last}},{{/-last}}{{/optionalParams}}); } {{/optionalParams.0}} @@ -131,7 +188,7 @@ public class {{classname}} extends ApiClient { * @param {{paramName}} {{{description}}}{{#required}} (required){{/required}}{{^required}} (optional{{^isContainer}}{{#defaultValue}}, default to {{.}}{{/defaultValue}}{{/isContainer}}){{/required}}{{/allParams}} * @param _callback The callback to be executed when the API call finishes * @return The request call - * @throws ApiException If fail to process the API call, e.g. serializing the request body object + * @throws AlgoliaRuntimeException If fail to process the API call, e.g. serializing the request body object {{#isDeprecated}} * @deprecated {{/isDeprecated}} @@ -143,7 +200,7 @@ public class {{classname}} extends ApiClient { {{#isDeprecated}} @Deprecated {{/isDeprecated}} - public Call {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ApiCallback<{{{returnType}}}{{^returnType}}Void{{/returnType}}> _callback) throws ApiException { + public Call {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}, {{/allParams}}final ApiCallback<{{{returnType}}}{{^returnType}}Void{{/returnType}}> _callback) throws AlgoliaRuntimeException { Call call = {{operationId}}ValidateBeforeCall({{#allParams}}{{paramName}}, {{/allParams}}_callback); {{#returnType}}Type returnType = new TypeToken<{{{returnType}}}>(){}.getType(); this.executeAsync(call, returnType, _callback);{{/returnType}}{{^returnType}}this.executeAsync(call, _callback);{{/returnType}} diff --git a/templates/java/libraries/okhttp-gson/apiException.mustache b/templates/java/libraries/okhttp-gson/apiException.mustache deleted file mode 100644 index f64a2f29ff..0000000000 --- a/templates/java/libraries/okhttp-gson/apiException.mustache +++ /dev/null @@ -1,93 +0,0 @@ -package {{invokerPackage}}; - -import java.util.Map; -import java.util.List; -{{#caseInsensitiveResponseHeaders}} -import java.util.Map.Entry; -import java.util.TreeMap; -{{/caseInsensitiveResponseHeaders}} - -public class ApiException extends{{#useRuntimeException}} RuntimeException {{/useRuntimeException}}{{^useRuntimeException}} Exception {{/useRuntimeException}}{ - private int code = 0; - private Map> responseHeaders = null; - private String responseBody = null; - - public ApiException() {} - - public ApiException(Throwable throwable) { - super(throwable); - } - - public ApiException(String message) { - super(message); - } - - public ApiException(String message, Throwable throwable, int code, Map> responseHeaders, String responseBody) { - super(message, throwable); - this.code = code; - {{#caseInsensitiveResponseHeaders}} - Map> headers = new TreeMap>(String.CASE_INSENSITIVE_ORDER); - for(Entry> entry : responseHeaders.entrySet()){ - headers.put(entry.getKey().toLowerCase(), entry.getValue()); - } - {{/caseInsensitiveResponseHeaders}} - this.responseHeaders = {{#caseInsensitiveResponseHeaders}}headers{{/caseInsensitiveResponseHeaders}}{{^caseInsensitiveResponseHeaders}}responseHeaders{{/caseInsensitiveResponseHeaders}}; - this.responseBody = responseBody; - } - - public ApiException(String message, int code, Map> responseHeaders, String responseBody) { - this(message, (Throwable) null, code, responseHeaders, responseBody); - } - - public ApiException(String message, Throwable throwable, int code, Map> responseHeaders) { - this(message, throwable, code, responseHeaders, null); - } - - public ApiException(int code, Map> responseHeaders, String responseBody) { - this((String) null, (Throwable) null, code, responseHeaders, responseBody); - } - - public ApiException(int code, String message) { - super(message); - this.code = code; - } - - public ApiException(int code, String message, Map> responseHeaders, String responseBody) { - this(code, message); - {{#caseInsensitiveResponseHeaders}} - Map> headers = new TreeMap>(String.CASE_INSENSITIVE_ORDER); - for(Entry> entry : responseHeaders.entrySet()){ - headers.put(entry.getKey().toLowerCase(), entry.getValue()); - } - {{/caseInsensitiveResponseHeaders}} - this.responseHeaders = {{#caseInsensitiveResponseHeaders}}headers{{/caseInsensitiveResponseHeaders}}{{^caseInsensitiveResponseHeaders}}responseHeaders{{/caseInsensitiveResponseHeaders}}; - this.responseBody = responseBody; - } - - /** - * Get the HTTP status code. - * - * @return HTTP status code - */ - public int getCode() { - return code; - } - - /** - * Get the HTTP response headers. - * - * @return A map of list of string - */ - public Map> getResponseHeaders() { - return responseHeaders; - } - - /** - * Get the HTTP response body. - * - * @return Response body in the form of string - */ - public String getResponseBody() { - return responseBody; - } -}