From 2c67e173385559aae8ea29dfe56e71bbb3bc5b2c Mon Sep 17 00:00:00 2001 From: Joe Cavazos Date: Sun, 2 Feb 2025 09:44:12 -0600 Subject: [PATCH 1/3] #496: Update JDK version to 11. Remove okhttp dependency and replace usages with JDK 11 HttpClient. Import Apache CXF for server-side-event (SSE) handling. --- build.gradle.kts | 11 +- src/main/java/org/stellar/sdk/Server.java | 172 ++++++++++------- .../java/org/stellar/sdk/SorobanServer.java | 47 +++-- src/main/java/org/stellar/sdk/StringUtil.java | 13 ++ src/main/java/org/stellar/sdk/UriBuilder.java | 169 +++++++++++++++++ src/main/java/org/stellar/sdk/UriUtil.java | 22 +++ .../stellar/sdk/federation/Federation.java | 66 +++---- .../java/org/stellar/sdk/http/GetRequest.java | 9 + .../org/stellar/sdk/http/HttpRequest.java | 15 ++ .../org/stellar/sdk/http/IHttpClient.java | 9 + .../org/stellar/sdk/http/Jdk11HttpClient.java | 160 ++++++++++++++++ .../org/stellar/sdk/http/PostRequest.java | 37 ++++ .../org/stellar/sdk/http/StringResponse.java | 27 +++ .../stellar/sdk/http/sse/CloseListener.java | 3 + .../stellar/sdk/http/sse/CxfSseClient.java | 41 ++++ .../sdk/http/sse/CxfSseEventStream.java | 99 ++++++++++ .../org/stellar/sdk/http/sse/ISseClient.java | 16 ++ .../stellar/sdk/http/sse/ISseEventStream.java | 5 + .../sdk/http/sse/ISseInboundEvent.java | 3 + .../org/stellar/sdk/http/sse/SseContext.java | 17 ++ .../sdk/requests/AccountsRequestBuilder.java | 39 ++-- .../sdk/requests/AssetsRequestBuilder.java | 15 +- .../ClaimableBalancesRequestBuilder.java | 18 +- .../ClientIdentificationInterceptor.java | 24 --- .../sdk/requests/EffectsRequestBuilder.java | 17 +- .../sdk/requests/FeeStatsRequestBuilder.java | 9 +- .../sdk/requests/LedgersRequestBuilder.java | 19 +- .../LiquidityPoolsRequestBuilder.java | 19 +- .../sdk/requests/OffersRequestBuilder.java | 19 +- .../requests/OperationsRequestBuilder.java | 19 +- .../sdk/requests/OrderBookRequestBuilder.java | 17 +- .../sdk/requests/PaymentsRequestBuilder.java | 17 +- .../stellar/sdk/requests/RequestBuilder.java | 51 ++--- .../stellar/sdk/requests/ResponseHandler.java | 107 +++++------ .../sdk/requests/RootRequestBuilder.java | 9 +- .../org/stellar/sdk/requests/SSEStream.java | 179 ++++-------------- .../StrictReceivePathsRequestBuilder.java | 20 +- .../StrictSendPathsRequestBuilder.java | 22 ++- .../TradeAggregationsRequestBuilder.java | 18 +- .../sdk/requests/TradesRequestBuilder.java | 17 +- .../requests/TransactionsRequestBuilder.java | 20 +- .../java/org/stellar/sdk/responses/Page.java | 18 +- src/test/java/org/stellar/sdk/ServerTest.java | 39 ++-- .../org/stellar/sdk/SslCertificateUtils.java | 6 +- .../sdk/federation/FederationTest.java | 103 ++++------ .../requests/AccountsRequestBuilderTest.java | 13 +- .../requests/AssetsRequestBuilderTest.java | 3 +- .../ClaimableBalancesRequestBuilderTest.java | 14 +- ...ptorTest.java => ClientIdHeadersTest.java} | 17 +- .../requests/EffectsRequestBuilderTest.java | 13 +- .../sdk/requests/FeeRequestBuilderTest.java | 3 +- .../requests/LedgersRequestBuilderTest.java | 4 +- .../LiquidityPoolsRequestBuilderTest.java | 10 +- .../requests/OffersRequestBuilderTest.java | 21 +- .../OperationsRequestBuilderTest.java | 29 ++- .../requests/OrderBookRequestBuilderTest.java | 4 +- .../sdk/requests/PathsRequestBuilderTest.java | 9 +- .../requests/PaymentsRequestBuilderTest.java | 13 +- .../sdk/requests/ResponseHandlerTest.java | 11 +- .../TradeAggregationsRequestBuilderTest.java | 3 +- .../requests/TradesRequestBuilderTest.java | 9 +- .../TransactionsRequestBuilderTest.java | 13 +- 62 files changed, 1248 insertions(+), 723 deletions(-) create mode 100644 src/main/java/org/stellar/sdk/StringUtil.java create mode 100644 src/main/java/org/stellar/sdk/UriBuilder.java create mode 100644 src/main/java/org/stellar/sdk/UriUtil.java create mode 100644 src/main/java/org/stellar/sdk/http/GetRequest.java create mode 100644 src/main/java/org/stellar/sdk/http/HttpRequest.java create mode 100644 src/main/java/org/stellar/sdk/http/IHttpClient.java create mode 100644 src/main/java/org/stellar/sdk/http/Jdk11HttpClient.java create mode 100644 src/main/java/org/stellar/sdk/http/PostRequest.java create mode 100644 src/main/java/org/stellar/sdk/http/StringResponse.java create mode 100644 src/main/java/org/stellar/sdk/http/sse/CloseListener.java create mode 100644 src/main/java/org/stellar/sdk/http/sse/CxfSseClient.java create mode 100644 src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java create mode 100644 src/main/java/org/stellar/sdk/http/sse/ISseClient.java create mode 100644 src/main/java/org/stellar/sdk/http/sse/ISseEventStream.java create mode 100644 src/main/java/org/stellar/sdk/http/sse/ISseInboundEvent.java create mode 100644 src/main/java/org/stellar/sdk/http/sse/SseContext.java delete mode 100644 src/main/java/org/stellar/sdk/requests/ClientIdentificationInterceptor.java rename src/test/java/org/stellar/sdk/requests/{InterceptorTest.java => ClientIdHeadersTest.java} (67%) diff --git a/build.gradle.kts b/build.gradle.kts index fcfeb0c3f..f8bbf94c9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -14,8 +14,8 @@ group = "network.lightsail" version = "1.0.0" java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } spotless { @@ -33,12 +33,15 @@ repositories { dependencies { val okhttpVersion = "4.12.0" - implementation("com.squareup.okhttp3:okhttp:${okhttpVersion}") - implementation("com.squareup.okhttp3:okhttp-sse:${okhttpVersion}") implementation("com.moandjiezana.toml:toml4j:0.7.2") implementation("com.google.code.gson:gson:2.11.0") implementation("org.bouncycastle:bcprov-jdk18on:1.79") implementation("commons-codec:commons-codec:1.17.1") + implementation("org.jetbrains:annotations:26.0.2") + + // Dependencies for SSE. + implementation("org.apache.cxf:cxf-rt-rs-sse:4.1.0") + implementation("org.apache.cxf:cxf-rt-rs-client:4.1.0") testImplementation("org.mockito:mockito-core:5.14.2") testImplementation("com.squareup.okhttp3:mockwebserver:${okhttpVersion}") diff --git a/src/main/java/org/stellar/sdk/Server.java b/src/main/java/org/stellar/sdk/Server.java index cff43695f..f1b407ff8 100644 --- a/src/main/java/org/stellar/sdk/Server.java +++ b/src/main/java/org/stellar/sdk/Server.java @@ -4,34 +4,37 @@ import java.io.Closeable; import java.io.IOException; import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.temporal.ChronoUnit; import java.util.HashSet; +import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import lombok.Getter; import lombok.Setter; -import okhttp3.*; -import okhttp3.Response; -import org.stellar.sdk.exception.AccountRequiresMemoException; -import org.stellar.sdk.exception.BadRequestException; -import org.stellar.sdk.exception.ConnectionErrorException; -import org.stellar.sdk.exception.RequestTimeoutException; -import org.stellar.sdk.exception.TooManyRequestsException; -import org.stellar.sdk.operations.AccountMergeOperation; -import org.stellar.sdk.operations.Operation; -import org.stellar.sdk.operations.PathPaymentStrictReceiveOperation; -import org.stellar.sdk.operations.PathPaymentStrictSendOperation; -import org.stellar.sdk.operations.PaymentOperation; +import org.stellar.sdk.exception.*; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.Jdk11HttpClient; +import org.stellar.sdk.http.PostRequest; +import org.stellar.sdk.http.StringResponse; +import org.stellar.sdk.http.sse.ISseClient; +import org.stellar.sdk.operations.*; import org.stellar.sdk.requests.*; -import org.stellar.sdk.responses.*; +import org.stellar.sdk.responses.AccountResponse; +import org.stellar.sdk.responses.FeeStatsResponse; +import org.stellar.sdk.responses.SubmitTransactionAsyncResponse; +import org.stellar.sdk.responses.TransactionResponse; import org.stellar.sdk.xdr.CryptoKeyType; /** Main class used to connect to Horizon server. */ public class Server implements Closeable { - private final HttpUrl serverURI; - @Getter @Setter private OkHttpClient httpClient; + private final URI serverURI; + @Getter @Setter private IHttpClient httpClient; /** submitHttpClient is used only for submitting transactions. The read timeout is longer. */ - @Getter @Setter private OkHttpClient submitHttpClient; + @Getter @Setter private IHttpClient submitHttpClient; + + @Getter @Setter private ISseClient sseClient; /** * HORIZON_SUBMIT_TIMEOUT is a time in seconds after Horizon sends a timeout response after @@ -54,31 +57,43 @@ public class Server implements Closeable { * @param uri The URI of the Horizon server. */ public Server(String uri) { - this( - uri, - new OkHttpClient.Builder() - .addInterceptor(new ClientIdentificationInterceptor()) - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(), - new OkHttpClient.Builder() - .addInterceptor(new ClientIdentificationInterceptor()) - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(HORIZON_SUBMIT_TIMEOUT + 5, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build()); + this(uri, normalHttpClient(), submitHttpClient()); + } + + private static IHttpClient normalHttpClient() { + return new Jdk11HttpClient.Builder() + .withDefaultHeader("X-Client-Name", "java-stellar-sdk") + .withDefaultHeader("X-Client-Version", Util.getSdkVersion()) + .withConnectTimeout(10, ChronoUnit.SECONDS) + .withReadTimeout(30, ChronoUnit.SECONDS) + .withRetryOnConnectionFailure(true) + .build(); + } + + private static IHttpClient submitHttpClient() { + return new Jdk11HttpClient.Builder() + .withDefaultHeader("X-Client-Name", "java-stellar-sdk") + .withDefaultHeader("X-Client-Version", Util.getSdkVersion()) + .withConnectTimeout(10, ChronoUnit.SECONDS) + .withReadTimeout(HORIZON_SUBMIT_TIMEOUT + 5, ChronoUnit.SECONDS) + .withRetryOnConnectionFailure(true) + .build(); } /** * Constructs a new Server object with custom HTTP clients. * * @param serverURI The URI of the Horizon server. - * @param httpClient The OkHttpClient to use for general requests. - * @param submitHttpClient The OkHttpClient to use for submitting transactions. + * @param httpClient The IHttpClient to use for general requests. + * @param submitHttpClient The IHttpClient to use for submitting transactions. */ - public Server(String serverURI, OkHttpClient httpClient, OkHttpClient submitHttpClient) { - this.serverURI = HttpUrl.parse(serverURI); + public Server(String serverURI, IHttpClient httpClient, IHttpClient submitHttpClient) { + try { + this.serverURI = new URI(serverURI); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid URI: " + serverURI); + } + this.httpClient = httpClient; this.submitHttpClient = submitHttpClient; } @@ -114,77 +129,77 @@ public TransactionBuilderAccount loadAccount(String address) { * @return {@link RootRequestBuilder} instance. */ public RootRequestBuilder root() { - return new RootRequestBuilder(httpClient, serverURI); + return new RootRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link AccountsRequestBuilder} instance. */ public AccountsRequestBuilder accounts() { - return new AccountsRequestBuilder(httpClient, serverURI); + return new AccountsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link AssetsRequestBuilder} instance. */ public AssetsRequestBuilder assets() { - return new AssetsRequestBuilder(httpClient, serverURI); + return new AssetsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link ClaimableBalancesRequestBuilder} instance. */ public ClaimableBalancesRequestBuilder claimableBalances() { - return new ClaimableBalancesRequestBuilder(httpClient, serverURI); + return new ClaimableBalancesRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link EffectsRequestBuilder} instance. */ public EffectsRequestBuilder effects() { - return new EffectsRequestBuilder(httpClient, serverURI); + return new EffectsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link LedgersRequestBuilder} instance. */ public LedgersRequestBuilder ledgers() { - return new LedgersRequestBuilder(httpClient, serverURI); + return new LedgersRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link OffersRequestBuilder} instance. */ public OffersRequestBuilder offers() { - return new OffersRequestBuilder(httpClient, serverURI); + return new OffersRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link OperationsRequestBuilder} instance. */ public OperationsRequestBuilder operations() { - return new OperationsRequestBuilder(httpClient, serverURI); + return new OperationsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link FeeStatsResponse} instance. */ public FeeStatsRequestBuilder feeStats() { - return new FeeStatsRequestBuilder(httpClient, serverURI); + return new FeeStatsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link OrderBookRequestBuilder} instance. */ public OrderBookRequestBuilder orderBook() { - return new OrderBookRequestBuilder(httpClient, serverURI); + return new OrderBookRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link TradesRequestBuilder} instance. */ public TradesRequestBuilder trades() { - return new TradesRequestBuilder(httpClient, serverURI); + return new TradesRequestBuilder(httpClient, sseClient, serverURI); } /** @@ -198,42 +213,50 @@ public TradeAggregationsRequestBuilder tradeAggregations( long resolution, long offset) { return new TradeAggregationsRequestBuilder( - httpClient, serverURI, baseAsset, counterAsset, startTime, endTime, resolution, offset); + httpClient, + sseClient, + serverURI, + baseAsset, + counterAsset, + startTime, + endTime, + resolution, + offset); } /** * @return {@link StrictReceivePathsRequestBuilder} instance. */ public StrictReceivePathsRequestBuilder strictReceivePaths() { - return new StrictReceivePathsRequestBuilder(httpClient, serverURI); + return new StrictReceivePathsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link StrictSendPathsRequestBuilder} instance. */ public StrictSendPathsRequestBuilder strictSendPaths() { - return new StrictSendPathsRequestBuilder(httpClient, serverURI); + return new StrictSendPathsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link PaymentsRequestBuilder} instance. */ public PaymentsRequestBuilder payments() { - return new PaymentsRequestBuilder(httpClient, serverURI); + return new PaymentsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link TransactionsRequestBuilder} instance. */ public TransactionsRequestBuilder transactions() { - return new TransactionsRequestBuilder(httpClient, serverURI); + return new TransactionsRequestBuilder(httpClient, sseClient, serverURI); } /** * @return {@link LiquidityPoolsRequestBuilder} instance. */ public LiquidityPoolsRequestBuilder liquidityPools() { - return new LiquidityPoolsRequestBuilder(httpClient, serverURI); + return new LiquidityPoolsRequestBuilder(httpClient, sseClient, serverURI); } /** @@ -259,20 +282,22 @@ public LiquidityPoolsRequestBuilder liquidityPools() { * due to cancellation or connectivity problems, etc. */ public TransactionResponse submitTransactionXdr(String transactionXdr) { - HttpUrl transactionsURI = serverURI.newBuilder().addPathSegment("transactions").build(); - RequestBody requestBody = new FormBody.Builder().add("tx", transactionXdr).build(); - Request submitTransactionRequest = - new Request.Builder().url(transactionsURI).post(requestBody).build(); + final var transactionsURI = new UriBuilder(serverURI).addPathSegment("transactions").build(); + final var form = Map.of("tx", transactionXdr); + final var post = PostRequest.formBody(transactionsURI, form); TypeToken type = new TypeToken() {}; - ResponseHandler responseHandler = new ResponseHandler<>(type); - Response response; + StringResponse response; try { - response = this.submitHttpClient.newCall(submitTransactionRequest).execute(); + response = this.submitHttpClient.post(post); } catch (SocketTimeoutException e) { throw new RequestTimeoutException(e); } catch (IOException e) { - throw new ConnectionErrorException(e); + if (e.getMessage().contains("request timed out")) { + throw new RequestTimeoutException(e); + } else { + throw new ConnectionErrorException(e); + } } return responseHandler.handleResponse(response); } @@ -431,17 +456,17 @@ public TransactionResponse submitTransaction(FeeBumpTransaction transaction) { * a Transaction Asynchronously */ public SubmitTransactionAsyncResponse submitTransactionXdrAsync(String transactionXdr) { - HttpUrl transactionsURI = serverURI.newBuilder().addPathSegment("transactions_async").build(); - RequestBody requestBody = new FormBody.Builder().add("tx", transactionXdr).build(); - Request submitTransactionRequest = - new Request.Builder().url(transactionsURI).post(requestBody).build(); + final var transactionsURI = + new UriBuilder(serverURI).addPathSegment("transactions_async").build(); + final var form = Map.of("tx", transactionXdr); + final var post = PostRequest.formBody(transactionsURI, form); TypeToken type = new TypeToken() {}; ResponseHandler responseHandler = new ResponseHandler<>(type); - Response response; + StringResponse response; try { - response = this.submitHttpClient.newCall(submitTransactionRequest).execute(); + response = this.submitHttpClient.post(post); } catch (SocketTimeoutException e) { throw new RequestTimeoutException(e); } catch (IOException e) { @@ -653,9 +678,16 @@ private void checkMemoRequired(Transaction transaction) { @Override public void close() { - // workaround for https://github.com/square/okhttp/issues/3372 - // sometimes, the connection pool keeps running and this can prevent a clean shut down. - this.httpClient.connectionPool().evictAll(); - this.submitHttpClient.connectionPool().evictAll(); + try { + this.httpClient.close(); + } catch (Exception e) { + e.printStackTrace(); + } + + try { + this.submitHttpClient.close(); + } catch (Exception e) { + e.printStackTrace(); + } } } diff --git a/src/main/java/org/stellar/sdk/SorobanServer.java b/src/main/java/org/stellar/sdk/SorobanServer.java index 487e0a327..fae0f2511 100644 --- a/src/main/java/org/stellar/sdk/SorobanServer.java +++ b/src/main/java/org/stellar/sdk/SorobanServer.java @@ -5,28 +5,25 @@ import java.io.Closeable; import java.io.IOException; import java.net.SocketTimeoutException; +import java.net.URI; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.UUID; -import java.util.concurrent.TimeUnit; -import okhttp3.HttpUrl; -import okhttp3.MediaType; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.RequestBody; -import okhttp3.Response; import org.jetbrains.annotations.Nullable; import org.stellar.sdk.exception.AccountNotFoundException; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.PrepareTransactionException; import org.stellar.sdk.exception.RequestTimeoutException; import org.stellar.sdk.exception.SorobanRpcException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.Jdk11HttpClient; +import org.stellar.sdk.http.PostRequest; import org.stellar.sdk.operations.InvokeHostFunctionOperation; import org.stellar.sdk.operations.Operation; -import org.stellar.sdk.requests.ClientIdentificationInterceptor; import org.stellar.sdk.requests.ResponseHandler; import org.stellar.sdk.requests.sorobanrpc.GetEventsRequest; import org.stellar.sdk.requests.sorobanrpc.GetLedgerEntriesRequest; @@ -64,8 +61,8 @@ public class SorobanServer implements Closeable { private static final int SUBMIT_TRANSACTION_TIMEOUT = 60; // seconds private static final int CONNECT_TIMEOUT = 10; // seconds - private final HttpUrl serverURI; - private final OkHttpClient httpClient; + private final URI serverURI; + private final IHttpClient httpClient; private final Gson gson = new Gson(); /** @@ -76,11 +73,10 @@ public class SorobanServer implements Closeable { public SorobanServer(String serverURI) { this( serverURI, - new OkHttpClient.Builder() - .addInterceptor(new ClientIdentificationInterceptor()) - .connectTimeout(CONNECT_TIMEOUT, TimeUnit.SECONDS) - .readTimeout(SUBMIT_TRANSACTION_TIMEOUT, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) + new Jdk11HttpClient.Builder() + .withConnectTimeout(CONNECT_TIMEOUT, ChronoUnit.SECONDS) + .withReadTimeout(SUBMIT_TRANSACTION_TIMEOUT, ChronoUnit.SECONDS) + .withRetryOnConnectionFailure(true) .build()); } @@ -88,10 +84,10 @@ public SorobanServer(String serverURI) { * Creates a new SorobanServer instance. * * @param serverURI The URI of the Soroban-RPC instance to connect to. - * @param httpClient The {@link OkHttpClient} instance to use for requests. + * @param httpClient The {@link IHttpClient} instance to use for requests. */ - public SorobanServer(String serverURI, OkHttpClient httpClient) { - this.serverURI = HttpUrl.parse(serverURI); + public SorobanServer(String serverURI, IHttpClient httpClient) { + this.serverURI = UriUtil.toUri(serverURI); this.httpClient = httpClient; } @@ -655,12 +651,11 @@ private R sendRequest( String requestId = generateRequestId(); ResponseHandler> responseHandler = new ResponseHandler<>(responseType); SorobanRpcRequest sorobanRpcRequest = new SorobanRpcRequest<>(requestId, method, params); - MediaType mediaType = MediaType.parse("application/json"); - RequestBody requestBody = - RequestBody.create(gson.toJson(sorobanRpcRequest).getBytes(), mediaType); - Request request = new Request.Builder().url(this.serverURI).post(requestBody).build(); - try (Response response = this.httpClient.newCall(request).execute()) { + final var request = PostRequest.jsonBody(this.serverURI, gson.toJson(sorobanRpcRequest)); + + try { + final var response = httpClient.post(request); SorobanRpcResponse sorobanRpcResponse = responseHandler.handleResponse(response); if (sorobanRpcResponse.getError() != null) { SorobanRpcResponse.Error error = sorobanRpcResponse.getError(); @@ -676,7 +671,11 @@ private R sendRequest( @Override public void close() throws IOException { - this.httpClient.connectionPool().evictAll(); + try { + this.httpClient.close(); + } catch (Exception e) { + e.printStackTrace(); + } } private static String generateRequestId() { diff --git a/src/main/java/org/stellar/sdk/StringUtil.java b/src/main/java/org/stellar/sdk/StringUtil.java new file mode 100644 index 000000000..41f3bf22e --- /dev/null +++ b/src/main/java/org/stellar/sdk/StringUtil.java @@ -0,0 +1,13 @@ +package org.stellar.sdk; + +import java.util.List; + +public class StringUtil { + public static boolean isBlank(String s) { + return s == null || s.isBlank(); + } + + public static String join(List strings, String separator) { + return strings == null || strings.isEmpty() ? "" : String.join(separator, strings); + } +} diff --git a/src/main/java/org/stellar/sdk/UriBuilder.java b/src/main/java/org/stellar/sdk/UriBuilder.java new file mode 100644 index 000000000..f6197a4fb --- /dev/null +++ b/src/main/java/org/stellar/sdk/UriBuilder.java @@ -0,0 +1,169 @@ +package org.stellar.sdk; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import lombok.Getter; + +@Getter +public class UriBuilder { + private String scheme; + private String host; + private Integer port; + private final LinkedList pathSegments; + private final LinkedHashMap queryParams = new LinkedHashMap<>(); + + public UriBuilder(String uriString) { + this(UriUtil.toUri(uriString)); + } + + public UriBuilder(URI uri) { + this.scheme = uri.getScheme(); + this.host = uri.getHost(); + this.port = uri.getPort() < 0 ? null : uri.getPort(); + this.pathSegments = splitPathParts(uri.getPath()); + this.queryParams.putAll(parseQueryParams(uri.getQuery())); + } + + public URI build() { + final var builder = new StringBuilder(); + builder.append(scheme).append("://").append(host); + + if (port != null) { + builder.append(":").append(port); + } + + if (!pathSegments.isEmpty()) { + pathSegments.forEach( + s -> { + if (builder.charAt(builder.length() - 1) != '/') { + builder.append("/"); + } + + builder.append(s); + }); + } + + if (!queryParams.isEmpty()) { + builder.append("?"); + queryParams.forEach( + (name, value) -> { + if (builder.charAt(builder.length() - 1) != '?') { + builder.append("&"); + } + + builder.append(name); + + if (value != null) { + builder.append("=").append(value); + } + }); + } + + try { + return new URI(builder.toString()); + } catch (URISyntaxException e) { + throw new RuntimeException("Invalid URI: " + builder, e); + } + } + + public void removeAllQueryParameters(String name) { + queryParams.remove(name); + } + + public UriBuilder setScheme(String scheme) { + if (StringUtil.isBlank(scheme)) { + throw new IllegalArgumentException("Scheme cannot be blank."); + } + + this.scheme = scheme; + return this; + } + + public UriBuilder setHost(String host) { + if (StringUtil.isBlank(host)) { + throw new IllegalArgumentException("Host cannot be blank."); + } + + this.host = host; + return this; + } + + public UriBuilder setPort(Integer port) { + if (port != null && (port < 1 || port > 65535)) { + throw new IllegalArgumentException("Port must be a positive number between 1 and 65535."); + } + + this.port = port; + return this; + } + + public boolean hasQueryParameter(String name) { + return getQueryParameter(name).isPresent(); + } + + public Optional getQueryParameter(String name) { + return Optional.ofNullable(queryParams.get(name)); + } + + public UriBuilder setQueryParameter(String name, String value) { + if (StringUtil.isBlank(name)) { + throw new IllegalArgumentException("Query parameter name cannot be blank."); + } + + name = urlEncode(name.trim()); + value = value != null ? urlEncode(value) : null; + + this.queryParams.put(name, value); + return this; + } + + public UriBuilder addPathSegment(String pathSegment) { + if (StringUtil.isBlank(pathSegment)) { + throw new IllegalArgumentException("Path segment cannot be blank."); + } + + pathSegments.add(pathSegment); + return this; + } + + private static LinkedHashMap parseQueryParams(String queryParamsString) { + if (StringUtil.isBlank(queryParamsString)) { + return new LinkedHashMap<>(); + } else { + final var paramsMap = new LinkedHashMap(); + final var pattern = Pattern.compile("\\??(?:&?[^=&]*=[^=&]*)*"); + final var matcher = pattern.matcher(queryParamsString); + while (matcher.find()) { + final var parts = matcher.group(1).split("=", 2); + if (parts.length == 1 || StringUtil.isBlank(parts[1])) { + paramsMap.put(parts[0], ""); + } else { + paramsMap.put(parts[0], parts[1]); + } + } + + return paramsMap; + } + } + + private static LinkedList splitPathParts(String path) { + if (StringUtil.isBlank(path)) { + return new LinkedList<>(); + } else { + if (path.startsWith("/")) { + path = path.substring(1); + } + + return Arrays.stream(path.split("/")).collect(Collectors.toCollection(LinkedList::new)); + } + } + + private static String urlEncode(String input) { + return input == null ? null : URLEncoder.encode(input, StandardCharsets.UTF_8); + } +} diff --git a/src/main/java/org/stellar/sdk/UriUtil.java b/src/main/java/org/stellar/sdk/UriUtil.java new file mode 100644 index 000000000..0e3fe1a8a --- /dev/null +++ b/src/main/java/org/stellar/sdk/UriUtil.java @@ -0,0 +1,22 @@ +package org.stellar.sdk; + +import java.net.URI; +import java.net.URISyntaxException; +import org.stellar.sdk.exception.UnexpectedException; + +public class UriUtil { + /** + * Convenience method for parsing a URI string and handling a malformed URI with a helpful error + * message. + * + * @param uriString the URI string + * @return a URI object + */ + public static URI toUri(String uriString) { + try { + return new URI(uriString); + } catch (URISyntaxException e) { + throw new UnexpectedException("Bad URI: " + uriString); + } + } +} diff --git a/src/main/java/org/stellar/sdk/federation/Federation.java b/src/main/java/org/stellar/sdk/federation/Federation.java index ae246be92..c024ce329 100644 --- a/src/main/java/org/stellar/sdk/federation/Federation.java +++ b/src/main/java/org/stellar/sdk/federation/Federation.java @@ -3,18 +3,18 @@ import com.google.gson.reflect.TypeToken; import com.moandjiezana.toml.Toml; import java.io.IOException; -import java.util.concurrent.TimeUnit; +import java.net.URI; import lombok.NonNull; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; +import org.stellar.sdk.UriBuilder; +import org.stellar.sdk.UriUtil; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.TooManyRequestsException; import org.stellar.sdk.federation.exception.FederationServerInvalidException; import org.stellar.sdk.federation.exception.NoFederationServerException; import org.stellar.sdk.federation.exception.NotFoundException; import org.stellar.sdk.federation.exception.StellarTomlNotFoundInvalidException; +import org.stellar.sdk.http.GetRequest; +import org.stellar.sdk.http.IHttpClient; import org.stellar.sdk.requests.ResponseHandler; /** @@ -24,22 +24,17 @@ * href="https://developers.stellar.org/docs/learn/encyclopedia/network-configuration/federation">Federation */ public class Federation { - private final OkHttpClient httpClient; + private final IHttpClient httpClient; /** * Creates a new Federation instance. * - * @param httpClient OkHttpClient + * @param httpClient IHttpClient */ - public Federation(OkHttpClient httpClient) { + public Federation(IHttpClient httpClient) { this.httpClient = httpClient; } - /** Creates a new Federation instance with a default OkHttpClient. */ - public Federation() { - this.httpClient = createHttpClient(); - } - /** * Resolves the given address to federation record if the user was found for a given Stellar * address. @@ -110,19 +105,21 @@ public FederationResponse resolveAccountId(String accountId, String domain) { } private FederationResponse resolve(String q, String domain, QueryType queryType) { - HttpUrl federationServerUri = getFederationServerUri(domain); + final var federationServerUri = getFederationServerUri(domain); - HttpUrl.Builder uriBuilder = federationServerUri.newBuilder(); + final var uriBuilder = new UriBuilder(federationServerUri); uriBuilder.setQueryParameter("type", queryType.getValue()); uriBuilder.setQueryParameter("q", q); - HttpUrl uri = uriBuilder.build(); + final var uri = uriBuilder.build(); TypeToken type = new TypeToken() {}; ResponseHandler responseHandler = new ResponseHandler<>(type); - Request request = new Request.Builder().get().url(uri).build(); - try (Response response = this.httpClient.newCall(request).execute()) { - if (response.code() == 404) { + final var request = new GetRequest(uri); + try { + final var response = httpClient.get(request); + + if (response.getStatusCode() == 404) { throw new NotFoundException(); } @@ -145,23 +142,24 @@ private FederationResponse resolve(String q, String domain, QueryType queryType) * @throws ConnectionErrorException When the request cannot be executed due to cancellation or * connectivity problems, etc. */ - private HttpUrl getFederationServerUri(@NonNull String domain) { + private URI getFederationServerUri(@NonNull String domain) { String uriBuilder = "https://" + domain + "/.well-known/stellar.toml"; - HttpUrl stellarTomlUri = HttpUrl.parse(uriBuilder); - assert stellarTomlUri != null; - Request request = new Request.Builder().get().url(stellarTomlUri).build(); - try (Response response = httpClient.newCall(request).execute()) { - if (response.code() >= 300) { - throw new StellarTomlNotFoundInvalidException(response.code()); + URI stellarTomlUri = UriUtil.toUri(uriBuilder); + final var request = new GetRequest(stellarTomlUri); + try { + final var response = httpClient.get(request); + + if (response.getStatusCode() >= 300) { + throw new StellarTomlNotFoundInvalidException(response.getStatusCode()); } - assert response.body() != null; - Toml stellarToml = new Toml().read(response.body().string()); + assert response.getResponseBody() != null; + Toml stellarToml = new Toml().read(response.getResponseBody()); String federationServer = stellarToml.getString("FEDERATION_SERVER"); if (federationServer == null || federationServer.isEmpty()) { throw new NoFederationServerException(); } - HttpUrl federationServerUrl = HttpUrl.parse(federationServer); - if (federationServerUrl == null || (!federationServerUrl.isHttps())) { + URI federationServerUrl = UriUtil.toUri(federationServer); + if (!federationServerUrl.getScheme().equalsIgnoreCase("https")) { throw new FederationServerInvalidException(); } return federationServerUrl; @@ -170,14 +168,6 @@ private HttpUrl getFederationServerUri(@NonNull String domain) { } } - private static OkHttpClient createHttpClient() { - return new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .retryOnConnectionFailure(false) - .build(); - } - private enum QueryType { NAME("name"), ID("id"); diff --git a/src/main/java/org/stellar/sdk/http/GetRequest.java b/src/main/java/org/stellar/sdk/http/GetRequest.java new file mode 100644 index 000000000..73162fe61 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/GetRequest.java @@ -0,0 +1,9 @@ +package org.stellar.sdk.http; + +import java.net.URI; + +public class GetRequest extends HttpRequest { + public GetRequest(URI uri) { + super(uri); + } +} diff --git a/src/main/java/org/stellar/sdk/http/HttpRequest.java b/src/main/java/org/stellar/sdk/http/HttpRequest.java new file mode 100644 index 000000000..de50f7591 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/HttpRequest.java @@ -0,0 +1,15 @@ +package org.stellar.sdk.http; + +import java.net.URI; +import java.util.LinkedHashMap; +import lombok.Getter; + +@Getter +public abstract class HttpRequest { + protected final URI uri; + protected final LinkedHashMap requestHeaders = new LinkedHashMap<>(); + + protected HttpRequest(URI uri) { + this.uri = uri; + } +} diff --git a/src/main/java/org/stellar/sdk/http/IHttpClient.java b/src/main/java/org/stellar/sdk/http/IHttpClient.java new file mode 100644 index 000000000..05f5394e2 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/IHttpClient.java @@ -0,0 +1,9 @@ +package org.stellar.sdk.http; + +import java.io.IOException; + +public interface IHttpClient extends AutoCloseable { + StringResponse get(GetRequest request) throws IOException; + + StringResponse post(PostRequest request) throws IOException; +} diff --git a/src/main/java/org/stellar/sdk/http/Jdk11HttpClient.java b/src/main/java/org/stellar/sdk/http/Jdk11HttpClient.java new file mode 100644 index 000000000..88ac834ae --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/Jdk11HttpClient.java @@ -0,0 +1,160 @@ +package org.stellar.sdk.http; + +import java.io.IOException; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.concurrent.ExecutorService; +import javax.net.ssl.SSLContext; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; +import org.stellar.sdk.StringUtil; + +@AllArgsConstructor +public class Jdk11HttpClient implements IHttpClient { + public static class Builder { + private final Map> defaultHeaders = new HashMap<>(); + private Duration connectTimeout = Duration.of(10, ChronoUnit.SECONDS); + private Duration readTimeout = Duration.of(30, ChronoUnit.SECONDS); + private boolean retryOnConnectionFailure = false; + private SSLContext sslContext; + + public Builder withSslContext(SSLContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public Builder withConnectTimeout(long amount, @NotNull ChronoUnit chronoUnit) { + if (amount < 0) { + throw new IllegalArgumentException("Timeout cannot be negative."); + } + + this.connectTimeout = Duration.of(amount, chronoUnit); + return this; + } + + public Builder withReadTimeout(long amount, @NotNull ChronoUnit chronoUnit) { + if (amount < 0) { + throw new IllegalArgumentException("Timeout cannot be negative."); + } + + this.readTimeout = Duration.of(amount, chronoUnit); + return this; + } + + public Builder withRetryOnConnectionFailure(boolean retryOnConnectionFailure) { + this.retryOnConnectionFailure = retryOnConnectionFailure; + return this; + } + + public Builder withDefaultHeader(@NotNull String name, String value) { + if (name.isBlank()) { + throw new IllegalArgumentException("Header name cannot be blank."); + } + + defaultHeaders.put(name, new LinkedList<>(List.of(value))); + return this; + } + + public Jdk11HttpClient build() { + final var builder = HttpClient.newBuilder().connectTimeout(connectTimeout); + + if (sslContext != null) { + builder.sslContext(sslContext); + } + + return new Jdk11HttpClient( + builder.build(), readTimeout, retryOnConnectionFailure, defaultHeaders); + } + } + + private final HttpClient httpClient; + private final Duration readTimeout; + + // TODO: Implement retry logic from OkHttp. + private final boolean retryOnConnectionFailure; + + private final Map> defaultHeaders; + + @Override + public void close() throws Exception { + if (httpClient != null) { + httpClient + .executor() + .ifPresent( + executor -> { + if (executor instanceof ExecutorService) { + ((ExecutorService) executor).shutdownNow(); + } + }); + } + } + + @Override + public StringResponse get(GetRequest request) throws IOException { + final var builder = HttpRequest.newBuilder(request.getUri()).GET().timeout(readTimeout); + + applyDefaultHeaders(builder); + + return execute(builder.build()); + } + + @Override + public StringResponse post(PostRequest request) throws IOException { + final var builder = + HttpRequest.newBuilder(request.getUri()) + .POST( + request.isEmptyBody() + ? HttpRequest.BodyPublishers.noBody() + : HttpRequest.BodyPublishers.ofString(request.getRequestBody())) + .header("Content-Type", request.getContentType()) + .timeout(readTimeout); + + applyDefaultHeaders(builder); + + return execute(builder.build()); + } + + private StringResponse execute(HttpRequest request) throws IOException { + try { + return toStringResponse(httpClient.send(request, HttpResponse.BodyHandlers.ofString())); + } catch (InterruptedException e) { + throw new RuntimeException("Interrupted during HTTP call.", e); + } + } + + private void applyDefaultHeaders(HttpRequest.Builder builder) { + if (defaultHeaders != null && !defaultHeaders.isEmpty()) { + defaultHeaders.forEach( + (name, values) -> { + final var valueString = StringUtil.join(values, ","); + builder.header(name, valueString); + }); + } + } + + private static StringResponse toStringResponse(HttpResponse responseBody) { + return StringResponse.builder() + .responseBody(responseBody.body()) + .statusCode(responseBody.statusCode()) + .headers(toHeadersMap(responseBody.headers())) + .build(); + } + + private static Map> toHeadersMap(HttpHeaders headers) { + final var returnMap = new HashMap>(); + + headers + .map() + .forEach( + (name, values) -> { + returnMap.put(name, values == null ? new LinkedList<>() : new LinkedList<>(values)); + }); + + return returnMap; + } +} diff --git a/src/main/java/org/stellar/sdk/http/PostRequest.java b/src/main/java/org/stellar/sdk/http/PostRequest.java new file mode 100644 index 000000000..2763c5e52 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/PostRequest.java @@ -0,0 +1,37 @@ +package org.stellar.sdk.http; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; + +@Getter +public class PostRequest extends HttpRequest { + private final String requestBody; + private final String contentType; + + public PostRequest(URI uri, String requestBody, String contentType) { + super(uri); + this.requestBody = requestBody; + this.contentType = contentType; + } + + public static PostRequest formBody(URI uri, Map formFields) { + final var formString = + formFields.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + + return new PostRequest(uri, formString, "application/x-www-form-urlencoded"); + } + + public static PostRequest jsonBody(URI uri, String json) { + return new PostRequest(uri, json, "application/json"); + } + + public boolean isEmptyBody() { + return requestBody == null; + } +} diff --git a/src/main/java/org/stellar/sdk/http/StringResponse.java b/src/main/java/org/stellar/sdk/http/StringResponse.java new file mode 100644 index 000000000..2adf8d300 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/StringResponse.java @@ -0,0 +1,27 @@ +package org.stellar.sdk.http; + +import java.util.LinkedList; +import java.util.Map; +import java.util.Optional; +import lombok.Builder; +import lombok.Getter; +import org.stellar.sdk.StringUtil; + +@Getter +@Builder +public class StringResponse { + private final String responseBody; + private final int statusCode; + private final Map> headers; + + public Optional getHeader(String name) { + if (headers == null || headers.isEmpty() || StringUtil.isBlank(name)) { + return Optional.empty(); + } + + return headers.entrySet().stream() + .filter(header -> header.getKey().equalsIgnoreCase(name)) + .map(header -> String.join(",", header.getValue())) + .findFirst(); + } +} diff --git a/src/main/java/org/stellar/sdk/http/sse/CloseListener.java b/src/main/java/org/stellar/sdk/http/sse/CloseListener.java new file mode 100644 index 000000000..49c582570 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/sse/CloseListener.java @@ -0,0 +1,3 @@ +package org.stellar.sdk.http.sse; + +public interface CloseListener {} diff --git a/src/main/java/org/stellar/sdk/http/sse/CxfSseClient.java b/src/main/java/org/stellar/sdk/http/sse/CxfSseClient.java new file mode 100644 index 000000000..6ceec5866 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/sse/CxfSseClient.java @@ -0,0 +1,41 @@ +package org.stellar.sdk.http.sse; + +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.sse.SseEventSource; +import java.net.URI; +import org.stellar.sdk.requests.EventListener; +import org.stellar.sdk.requests.RequestBuilder; + +public class CxfSseClient implements ISseClient { + private Client client; + + public CxfSseClient() { + this.client = ClientBuilder.newClient(); + } + + @Override + public ISseEventStream createEventStream( + SseContext context, + Class responseClass, + long listenerId, + EventListener eventListener, + RequestBuilder requestBuilder, + URI uri, + String lastEventId) { + final var sseEventSource = createEventSource(uri, lastEventId); + + return new CxfSseEventStream<>( + context, sseEventSource, responseClass, listenerId, eventListener, requestBuilder); + } + + private SseEventSource createEventSource(URI uri, String lastEventId) { + final var webTarget = client.target(uri); + + if (lastEventId != null) { + webTarget.request().header("Last-Event-ID", lastEventId); + } + + return SseEventSource.target(webTarget).build(); + } +} diff --git a/src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java b/src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java new file mode 100644 index 000000000..c6ef2b506 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java @@ -0,0 +1,99 @@ +package org.stellar.sdk.http.sse; + +import jakarta.ws.rs.sse.SseEventSource; +import java.net.SocketException; +import java.util.Optional; +import lombok.Getter; +import org.stellar.sdk.requests.EventListener; +import org.stellar.sdk.requests.RequestBuilder; +import org.stellar.sdk.responses.Pageable; +import org.stellar.sdk.responses.gson.GsonSingleton; + +@Getter +public class CxfSseEventStream + implements ISseEventStream { + private final SseContext context; + private final SseEventSource eventSource; + private final Class responseClass; + private final long listenerId; + private final EventListener eventListener; + private final RequestBuilder requestBuilder; + + public CxfSseEventStream( + SseContext context, + SseEventSource eventSource, + Class responseClass, + long listenerId, + EventListener eventListener, + RequestBuilder requestBuilder) { + this.context = context; + this.eventSource = eventSource; + this.responseClass = responseClass; + this.listenerId = listenerId; + this.eventListener = eventListener; + this.requestBuilder = requestBuilder; + this.eventSource.register( + inboundSseEvent -> { + // onEvent + if (context.getIsStopped().get() || listenerId != context.getCurrentListenerId().get()) { + return; + } + + context.getLastEventTime().set(System.currentTimeMillis()); + + final var eventData = inboundSseEvent.readData(); + + if (eventData.equals("\"hello\"") || eventData.equals("\"byebye\"")) { + return; + } + + T event = GsonSingleton.getInstance().fromJson(eventData, responseClass); + + if (event instanceof Pageable) { + String pagingToken = ((Pageable) event).getPagingToken(); + requestBuilder.cursor(pagingToken); + } + + context.getLastEventId().set(inboundSseEvent.getId()); + eventListener.onEvent(event); + }, + throwable -> { + // onError + if (context.getIsStopped().get() || listenerId != context.getCurrentListenerId().get()) { + return; + } + + Optional code = Optional.empty(); + + // TODO: Figure out how to produce a code. + // if (response != null) { + // code = Optional.of(response.code()); + // } + + if (throwable != null) { + if (throwable instanceof SocketException) { + context.getIsClosed().compareAndSet(false, true); + } else { + eventListener.onFailure(Optional.of(throwable), code); + } + } else { + eventListener.onFailure(Optional.empty(), code); + } + }, + () -> { + // onComplete + if (context.getIsStopped().get() || listenerId != context.getCurrentListenerId().get()) { + return; + } + + context.getIsClosed().compareAndSet(false, true); + }); + } + + @Override + public void cancel() { + if (eventSource != null) { + eventSource.close(); + } + } +} diff --git a/src/main/java/org/stellar/sdk/http/sse/ISseClient.java b/src/main/java/org/stellar/sdk/http/sse/ISseClient.java new file mode 100644 index 000000000..b8b2aec91 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/sse/ISseClient.java @@ -0,0 +1,16 @@ +package org.stellar.sdk.http.sse; + +import java.net.URI; +import org.stellar.sdk.requests.EventListener; +import org.stellar.sdk.requests.RequestBuilder; + +public interface ISseClient { + ISseEventStream createEventStream( + SseContext context, + Class responseClass, + long listenerId, + EventListener eventListener, + RequestBuilder requestBuilder, + URI uri, + String lastEventId); +} diff --git a/src/main/java/org/stellar/sdk/http/sse/ISseEventStream.java b/src/main/java/org/stellar/sdk/http/sse/ISseEventStream.java new file mode 100644 index 000000000..b6156617e --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/sse/ISseEventStream.java @@ -0,0 +1,5 @@ +package org.stellar.sdk.http.sse; + +public interface ISseEventStream { + void cancel(); +} diff --git a/src/main/java/org/stellar/sdk/http/sse/ISseInboundEvent.java b/src/main/java/org/stellar/sdk/http/sse/ISseInboundEvent.java new file mode 100644 index 000000000..e26e38ab3 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/sse/ISseInboundEvent.java @@ -0,0 +1,3 @@ +package org.stellar.sdk.http.sse; + +public interface ISseInboundEvent {} diff --git a/src/main/java/org/stellar/sdk/http/sse/SseContext.java b/src/main/java/org/stellar/sdk/http/sse/SseContext.java new file mode 100644 index 000000000..9c673db94 --- /dev/null +++ b/src/main/java/org/stellar/sdk/http/sse/SseContext.java @@ -0,0 +1,17 @@ +package org.stellar.sdk.http.sse; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SseContext { + private final AtomicBoolean isClosed; + private final AtomicBoolean isStopped; + private final AtomicLong currentListenerId; + private final AtomicLong lastEventTime; + private final AtomicReference lastEventId; +} diff --git a/src/main/java/org/stellar/sdk/requests/AccountsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/AccountsRequestBuilder.java index 3471cdc95..2c663eec7 100644 --- a/src/main/java/org/stellar/sdk/requests/AccountsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/AccountsRequestBuilder.java @@ -1,12 +1,13 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.AssetTypeCreditAlphaNum; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.RequestTimeoutException; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.AccountResponse; import org.stellar.sdk.responses.Page; @@ -17,8 +18,8 @@ public class AccountsRequestBuilder extends RequestBuilder { private static final String SIGNER_PARAMETER_NAME = "signer"; private static final String SPONSOR_PARAMETER_NAME = "sponsor"; - public AccountsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "accounts"); + public AccountsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "accounts"); } /** @@ -40,7 +41,7 @@ public AccountsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { * @throws ConnectionErrorException When the request cannot be executed due to cancellation or * connectivity problems, etc. */ - public AccountResponse account(HttpUrl uri) { + public AccountResponse account(URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -79,13 +80,13 @@ public AccountResponse account(String account) { * @see Accounts */ public AccountsRequestBuilder forSigner(String signer) { - if (uriBuilder.build().queryParameter(ASSET_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(ASSET_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both asset and signer"); } - if (uriBuilder.build().queryParameter(LIQUIDITY_POOL_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(LIQUIDITY_POOL_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both liquidity_pool and signer"); } - if (uriBuilder.build().queryParameter(SPONSOR_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(SPONSOR_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both sponsor and signer"); } uriBuilder.setQueryParameter(SIGNER_PARAMETER_NAME, signer); @@ -100,13 +101,13 @@ public AccountsRequestBuilder forSigner(String signer) { * @see Accounts */ public AccountsRequestBuilder forAsset(AssetTypeCreditAlphaNum asset) { - if (uriBuilder.build().queryParameter(LIQUIDITY_POOL_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(LIQUIDITY_POOL_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both liquidity_pool and asset"); } - if (uriBuilder.build().queryParameter(SIGNER_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(SIGNER_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both signer and asset"); } - if (uriBuilder.build().queryParameter(SPONSOR_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(SPONSOR_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both sponsor and asset"); } setAssetParameter(ASSET_PARAMETER_NAME, asset); @@ -121,13 +122,13 @@ public AccountsRequestBuilder forAsset(AssetTypeCreditAlphaNum asset) { * @see Accounts */ public AccountsRequestBuilder forLiquidityPool(String liquidityPoolId) { - if (uriBuilder.build().queryParameter(ASSET_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(ASSET_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both asset and liquidity_pool"); } - if (uriBuilder.build().queryParameter(SIGNER_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(SIGNER_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both signer and liquidity_pool"); } - if (uriBuilder.build().queryParameter(SPONSOR_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(SPONSOR_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both sponsor and liquidity_pool"); } uriBuilder.setQueryParameter(LIQUIDITY_POOL_PARAMETER_NAME, liquidityPoolId); @@ -143,13 +144,13 @@ public AccountsRequestBuilder forLiquidityPool(String liquidityPoolId) { * @see Accounts */ public AccountsRequestBuilder forSponsor(String sponsor) { - if (uriBuilder.build().queryParameter(ASSET_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(ASSET_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both asset and sponsor"); } - if (uriBuilder.build().queryParameter(LIQUIDITY_POOL_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(LIQUIDITY_POOL_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both liquidity_pool and sponsor"); } - if (uriBuilder.build().queryParameter(SIGNER_PARAMETER_NAME) != null) { + if (uriBuilder.hasQueryParameter(SIGNER_PARAMETER_NAME)) { throw new IllegalArgumentException("cannot set both signer and sponsor"); } uriBuilder.setQueryParameter(SPONSOR_PARAMETER_NAME, sponsor); @@ -176,7 +177,7 @@ public AccountsRequestBuilder forSponsor(String sponsor) { * @throws ConnectionErrorException When the request cannot be executed due to cancellation or * connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -195,7 +196,7 @@ public static Page execute(OkHttpClient httpClient, HttpUrl uri */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, AccountResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, AccountResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/AssetsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/AssetsRequestBuilder.java index f69697010..668f0d790 100644 --- a/src/main/java/org/stellar/sdk/requests/AssetsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/AssetsRequestBuilder.java @@ -1,16 +1,17 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.AssetResponse; import org.stellar.sdk.responses.Page; public class AssetsRequestBuilder extends RequestBuilder { - public AssetsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "assets"); + public AssetsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "assets"); } public AssetsRequestBuilder assetCode(String assetCode) { @@ -27,8 +28,8 @@ public AssetsRequestBuilder assetIssuer(String assetIssuer) { * Requests specific uri and returns {@link Page} of {@link AssetResponse}. This * * method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link AssetResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -45,7 +46,7 @@ public AssetsRequestBuilder assetIssuer(String assetIssuer) { * @throws ConnectionErrorException When the request cannot be executed due to cancellation or * connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } diff --git a/src/main/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilder.java index bb22d8f60..9078281e5 100644 --- a/src/main/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilder.java @@ -1,19 +1,21 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.Asset; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.AssetResponse; import org.stellar.sdk.responses.ClaimableBalanceResponse; import org.stellar.sdk.responses.Page; /** Builds requests connected to claimable balances. */ public class ClaimableBalancesRequestBuilder extends RequestBuilder { - public ClaimableBalancesRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "claimable_balances"); + public ClaimableBalancesRequestBuilder( + IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "claimable_balances"); } /** @@ -36,7 +38,7 @@ public ClaimableBalancesRequestBuilder(OkHttpClient httpClient, HttpUrl serverUR * @throws ConnectionErrorException When the request cannot be executed due to cancellation or * connectivity problems, etc. */ - public ClaimableBalanceResponse claimableBalance(HttpUrl uri) { + public ClaimableBalanceResponse claimableBalance(URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -103,8 +105,8 @@ public ClaimableBalancesRequestBuilder forClaimant(String claimant) { * Requests specific uri and returns {@link Page} of {@link * ClaimableBalanceResponse}. This method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link ClaimableBalanceResponse} * @throws org.stellar.sdk.exception.BadRequestException if the request fails due to a bad request * (4xx) @@ -114,7 +116,7 @@ public ClaimableBalancesRequestBuilder forClaimant(String claimant) { * limited to a timeout, connection failure etc. * @throws TooManyRequestsException when too many requests were sent to the Horizon server. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); diff --git a/src/main/java/org/stellar/sdk/requests/ClientIdentificationInterceptor.java b/src/main/java/org/stellar/sdk/requests/ClientIdentificationInterceptor.java deleted file mode 100644 index 580ba4012..000000000 --- a/src/main/java/org/stellar/sdk/requests/ClientIdentificationInterceptor.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.stellar.sdk.requests; - -import java.io.IOException; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; -import org.stellar.sdk.Util; - -public class ClientIdentificationInterceptor implements Interceptor { - - @Override - public Response intercept(Chain chain) throws IOException { - Request originalRequest = chain.request(); - Request requestWithHeaders; - - requestWithHeaders = - originalRequest - .newBuilder() - .addHeader("X-Client-Name", "java-stellar-sdk") - .addHeader("X-Client-Version", Util.getSdkVersion()) - .build(); - return chain.proceed(requestWithHeaders); - } -} diff --git a/src/main/java/org/stellar/sdk/requests/EffectsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/EffectsRequestBuilder.java index 874c2d125..9f1cc4254 100644 --- a/src/main/java/org/stellar/sdk/requests/EffectsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/EffectsRequestBuilder.java @@ -1,18 +1,19 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; +import java.net.URI; import lombok.NonNull; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.effects.EffectResponse; /** Builds requests connected to effects. */ public class EffectsRequestBuilder extends RequestBuilder { - public EffectsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "effects"); + public EffectsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "effects"); } /** @@ -79,8 +80,8 @@ public EffectsRequestBuilder forOperation(long operationId) { * Requests specific uri and returns {@link Page} of {@link EffectResponse}. This * method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link EffectResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -97,7 +98,7 @@ public EffectsRequestBuilder forOperation(long operationId) { * @throws ConnectionErrorException When the request cannot be executed due to cancellation or * connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -116,7 +117,7 @@ public static Page execute(OkHttpClient httpClient, HttpUrl uri) */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, EffectResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, EffectResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/FeeStatsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/FeeStatsRequestBuilder.java index 631e2ef92..80655db8e 100644 --- a/src/main/java/org/stellar/sdk/requests/FeeStatsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/FeeStatsRequestBuilder.java @@ -1,15 +1,16 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.FeeStatsResponse; public class FeeStatsRequestBuilder extends RequestBuilder { - public FeeStatsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "fee_stats"); + public FeeStatsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "fee_stats"); } /** diff --git a/src/main/java/org/stellar/sdk/requests/LedgersRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/LedgersRequestBuilder.java index f19878d11..6ed0fdcca 100644 --- a/src/main/java/org/stellar/sdk/requests/LedgersRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/LedgersRequestBuilder.java @@ -1,16 +1,17 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.LedgerResponse; import org.stellar.sdk.responses.Page; /** Builds requests connected to ledgers. */ public class LedgersRequestBuilder extends RequestBuilder { - public LedgersRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "ledgers"); + public LedgersRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "ledgers"); } /** @@ -33,7 +34,7 @@ public LedgersRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public LedgerResponse ledger(HttpUrl uri) { + public LedgerResponse ledger(URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -68,8 +69,8 @@ public LedgerResponse ledger(long ledgerSeq) { * Requests specific uri and returns {@link Page} of {@link LedgerResponse}. This * method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link LedgerResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -86,7 +87,7 @@ public LedgerResponse ledger(long ledgerSeq) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -105,7 +106,7 @@ public static Page execute(OkHttpClient httpClient, HttpUrl uri) */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, LedgerResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, LedgerResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilder.java index 89dd7ddb0..da1c65412 100644 --- a/src/main/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilder.java @@ -1,9 +1,10 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.LiquidityPoolResponse; import org.stellar.sdk.responses.Page; @@ -12,8 +13,8 @@ public class LiquidityPoolsRequestBuilder extends RequestBuilder { private static final String RESERVES_PARAMETER_NAME = "reserves"; private static final String ACCOUNT_PARAMETER_NAME = "account"; - public LiquidityPoolsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "liquidity_pools"); + public LiquidityPoolsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "liquidity_pools"); } /** @@ -36,7 +37,7 @@ public LiquidityPoolsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public LiquidityPoolResponse liquidityPool(HttpUrl uri) { + public LiquidityPoolResponse liquidityPool(URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -91,8 +92,8 @@ public LiquidityPoolsRequestBuilder forAccount(String account) { * Requests specific uri and returns {@link Page} of {@link LiquidityPoolResponse}. * This method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link LiquidityPoolResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -109,7 +110,7 @@ public LiquidityPoolsRequestBuilder forAccount(String account) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -129,7 +130,7 @@ public static Page execute(OkHttpClient httpClient, HttpU public SSEStream stream( final EventListener listener, long reconnectTimeout) { return SSEStream.create( - httpClient, this, LiquidityPoolResponse.class, listener, reconnectTimeout); + sseClient, this, LiquidityPoolResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/OffersRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/OffersRequestBuilder.java index 430743d1c..f28ae0f8e 100644 --- a/src/main/java/org/stellar/sdk/requests/OffersRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/OffersRequestBuilder.java @@ -1,17 +1,18 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.Asset; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.OfferResponse; import org.stellar.sdk.responses.Page; /** Builds requests connected to offers. */ public class OffersRequestBuilder extends RequestBuilder { - public OffersRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "offers"); + public OffersRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "offers"); } /** @@ -34,7 +35,7 @@ public OffersRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public OfferResponse offer(HttpUrl uri) { + public OfferResponse offer(URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -116,8 +117,8 @@ public OffersRequestBuilder forSellingAsset(Asset asset) { * Requests specific uri and returns {@link Page} of {@link OfferResponse}. This * method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link OfferResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -134,7 +135,7 @@ public OffersRequestBuilder forSellingAsset(Asset asset) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -153,7 +154,7 @@ public static Page execute(OkHttpClient httpClient, HttpUrl uri) */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, OfferResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, OfferResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/OperationsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/OperationsRequestBuilder.java index 191665727..2295a25ad 100644 --- a/src/main/java/org/stellar/sdk/requests/OperationsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/OperationsRequestBuilder.java @@ -1,12 +1,13 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; +import java.net.URI; import java.util.HashSet; import java.util.Set; import lombok.NonNull; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.operations.OperationResponse; @@ -14,8 +15,8 @@ public class OperationsRequestBuilder extends RequestBuilder { protected Set toJoin; - public OperationsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "operations"); + public OperationsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "operations"); toJoin = new HashSet<>(); } @@ -39,7 +40,7 @@ public OperationsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public OperationResponse operation(HttpUrl uri) { + public OperationResponse operation(URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -172,8 +173,8 @@ protected void updateToJoin(String value, boolean include) { * Requests specific uri and returns {@link Page} of {@link OperationResponse}. This * method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link OperationResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -190,7 +191,7 @@ protected void updateToJoin(String value, boolean include) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -209,7 +210,7 @@ public static Page execute(OkHttpClient httpClient, HttpUrl u */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, OperationResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, OperationResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/OrderBookRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/OrderBookRequestBuilder.java index b23864144..8fcd6f95f 100644 --- a/src/main/java/org/stellar/sdk/requests/OrderBookRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/OrderBookRequestBuilder.java @@ -1,17 +1,18 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.Asset; import org.stellar.sdk.AssetTypeCreditAlphaNum; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.OrderBookResponse; /** Builds requests connected to order book. */ public class OrderBookRequestBuilder extends RequestBuilder { - public OrderBookRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "order_book"); + public OrderBookRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "order_book"); } public OrderBookRequestBuilder buyingAsset(Asset asset) { @@ -37,8 +38,8 @@ public OrderBookRequestBuilder sellingAsset(Asset asset) { /** * Requests specific uri and returns {@link OrderBookResponse}. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link OrderBookResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -55,7 +56,7 @@ public OrderBookRequestBuilder sellingAsset(Asset asset) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static OrderBookResponse execute(OkHttpClient httpClient, HttpUrl uri) { + public static OrderBookResponse execute(IHttpClient httpClient, URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -74,7 +75,7 @@ public static OrderBookResponse execute(OkHttpClient httpClient, HttpUrl uri) { */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, OrderBookResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, OrderBookResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/PaymentsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/PaymentsRequestBuilder.java index 592971a48..ff27db6c7 100644 --- a/src/main/java/org/stellar/sdk/requests/PaymentsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/PaymentsRequestBuilder.java @@ -1,12 +1,13 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; +import java.net.URI; import java.util.HashSet; import java.util.Set; import lombok.NonNull; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.operations.OperationResponse; @@ -14,8 +15,8 @@ public class PaymentsRequestBuilder extends RequestBuilder { protected Set toJoin; - public PaymentsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "payments"); + public PaymentsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "payments"); toJoin = new HashSet<>(); } @@ -83,8 +84,8 @@ protected void updateToJoin(String value, boolean include) { * Requests specific uri and returns {@link Page} of {@link OperationResponse}. This * method is helpful for getting the next set of results. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link OperationResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -101,7 +102,7 @@ protected void updateToJoin(String value, boolean include) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -120,7 +121,7 @@ public static Page execute(OkHttpClient httpClient, HttpUrl u */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, OperationResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, OperationResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/RequestBuilder.java b/src/main/java/org/stellar/sdk/requests/RequestBuilder.java index 3043b2193..47eca9533 100644 --- a/src/main/java/org/stellar/sdk/requests/RequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/RequestBuilder.java @@ -3,35 +3,37 @@ import com.google.gson.reflect.TypeToken; import java.io.IOException; import java.net.SocketTimeoutException; +import java.net.URI; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import org.stellar.sdk.Asset; -import org.stellar.sdk.AssetTypeCreditAlphaNum; -import org.stellar.sdk.AssetTypeNative; +import org.stellar.sdk.*; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.RequestTimeoutException; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.GetRequest; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.StringResponse; +import org.stellar.sdk.http.sse.ISseClient; /** Abstract class for request builders. */ public abstract class RequestBuilder { - protected HttpUrl.Builder uriBuilder; - protected OkHttpClient httpClient; + protected UriBuilder uriBuilder; + protected IHttpClient httpClient; + protected ISseClient sseClient; private final ArrayList segments; private boolean segmentsAdded; - RequestBuilder(OkHttpClient httpClient, HttpUrl serverURI, String defaultSegment) { + RequestBuilder( + IHttpClient httpClient, ISseClient sseClient, URI serverURI, String defaultSegment) { this.httpClient = httpClient; - uriBuilder = serverURI.newBuilder(); - segments = new ArrayList<>(); + this.sseClient = sseClient; + this.uriBuilder = new UriBuilder(serverURI); + this.segments = new ArrayList<>(); if (defaultSegment != null) { this.setSegments(defaultSegment); } - segmentsAdded = false; // Allow overwriting segments + this.segmentsAdded = false; // Allow overwriting segments } protected RequestBuilder setSegments(String... segments) { @@ -118,12 +120,14 @@ private String encodeAsset(Asset asset) { } } - HttpUrl buildUri() { + URI buildUri() { if (!segments.isEmpty()) { - for (String segment : segments) { - uriBuilder.addPathSegment(segment); - } + segments.forEach( + segment -> { + uriBuilder.addPathSegment(segment); + }); } + return uriBuilder.build(); } @@ -146,8 +150,8 @@ public String getValue() { * Executes a GET request and handles the response. * * @param The type of the response object - * @param httpClient The OkHttpClient to use for the request - * @param url The URL to send the GET request to + * @param httpClient The {@link IHttpClient} to use for the request + * @param uri The {@link URI} to send the GET request to * @param typeToken The TypeToken representing the type of the response * @return The response object of type T * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of @@ -165,13 +169,14 @@ public String getValue() { * @throws ConnectionErrorException When the request cannot be executed due to cancellation or * connectivity problems, etc. */ - static T executeGetRequest(OkHttpClient httpClient, HttpUrl url, TypeToken typeToken) { + static T executeGetRequest(IHttpClient httpClient, URI uri, TypeToken typeToken) { ResponseHandler responseHandler = new ResponseHandler<>(typeToken); - Request request = new Request.Builder().get().url(url).build(); - Response response; + final var getRequest = new GetRequest(uri); + + StringResponse response; try { - response = httpClient.newCall(request).execute(); + response = httpClient.get(getRequest); } catch (SocketTimeoutException e) { throw new RequestTimeoutException(e); } catch (IOException e) { diff --git a/src/main/java/org/stellar/sdk/requests/ResponseHandler.java b/src/main/java/org/stellar/sdk/requests/ResponseHandler.java index 7e999fac0..897c5bbff 100644 --- a/src/main/java/org/stellar/sdk/requests/ResponseHandler.java +++ b/src/main/java/org/stellar/sdk/requests/ResponseHandler.java @@ -1,14 +1,13 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import java.io.IOException; -import okhttp3.Response; import org.stellar.sdk.exception.BadRequestException; import org.stellar.sdk.exception.BadResponseException; import org.stellar.sdk.exception.RequestTimeoutException; import org.stellar.sdk.exception.TooManyRequestsException; import org.stellar.sdk.exception.UnexpectedException; import org.stellar.sdk.exception.UnknownResponseException; +import org.stellar.sdk.http.StringResponse; import org.stellar.sdk.responses.Problem; import org.stellar.sdk.responses.SubmitTransactionAsyncResponse; import org.stellar.sdk.responses.gson.GsonSingleton; @@ -43,7 +42,7 @@ public ResponseHandler(TypeToken type) { * @throws BadRequestException If the response code is in the 4xx range * @throws BadResponseException If the response code is in the 5xx range */ - public T handleResponse(final Response response) { + public T handleResponse(final StringResponse response) { return handleResponse(response, false); } @@ -61,76 +60,68 @@ public T handleResponse(final Response response) { * @throws BadRequestException If the response code is in the 4xx range * @throws BadResponseException If the response code is in the 5xx range */ - public T handleResponse(final Response response, boolean submitTransactionAsync) { - try { - // Too Many Requests - if (response.code() == 429) { + public T handleResponse(final StringResponse response, boolean submitTransactionAsync) { + // Too Many Requests + if (response.getStatusCode() == 429) { - Integer retryAfter = null; - String header = response.header("Retry-After"); - if (header != null) { - try { - retryAfter = Integer.parseInt(header); - } catch (NumberFormatException ignored) { - } + Integer retryAfter = null; + final var headerMaybe = response.getHeader("Retry-After"); + if (headerMaybe.isPresent()) { + try { + retryAfter = Integer.parseInt(headerMaybe.get()); + } catch (NumberFormatException ignored) { } - throw new TooManyRequestsException(retryAfter); } + throw new TooManyRequestsException(retryAfter); + } - String content = null; - if (response.body() == null) { - throw new UnexpectedException("Unexpected empty response body"); - } + String content = null; + if (response.getResponseBody() == null) { + throw new UnexpectedException("Unexpected empty response body"); + } - try { - content = response.body().string(); - } catch (IOException e) { - throw new UnexpectedException("Unexpected error reading response", e); + content = response.getResponseBody(); + + if (response.getStatusCode() >= 200 && response.getStatusCode() < 300) { + T object = GsonSingleton.getInstance().fromJson(content, type.getType()); + if (object instanceof TypedResponse) { + ((TypedResponse) object).setType(type); } + return object; + } - if (response.code() >= 200 && response.code() < 300) { - T object = GsonSingleton.getInstance().fromJson(content, type.getType()); - if (object instanceof TypedResponse) { - ((TypedResponse) object).setType(type); - } - return object; + // Other errors + if (response.getStatusCode() >= 400 && response.getStatusCode() < 600) { + Problem problem = null; + SubmitTransactionAsyncResponse submitTransactionAsyncProblem = null; + try { + problem = GsonSingleton.getInstance().fromJson(content, Problem.class); + } catch (Exception e) { + // if we can't parse the response, we just ignore it } - // Other errors - if (response.code() >= 400 && response.code() < 600) { - Problem problem = null; - SubmitTransactionAsyncResponse submitTransactionAsyncProblem = null; + if (submitTransactionAsync) { try { - problem = GsonSingleton.getInstance().fromJson(content, Problem.class); + submitTransactionAsyncProblem = + GsonSingleton.getInstance().fromJson(content, SubmitTransactionAsyncResponse.class); } catch (Exception e) { // if we can't parse the response, we just ignore it } - - if (submitTransactionAsync) { - try { - submitTransactionAsyncProblem = - GsonSingleton.getInstance().fromJson(content, SubmitTransactionAsyncResponse.class); - } catch (Exception e) { - // if we can't parse the response, we just ignore it - } - } - - if (response.code() == 504) { - throw new RequestTimeoutException(response.code(), content, problem); - } else if (response.code() < 500) { - // Codes in the 4xx range indicate an error that failed given the information provided - throw new BadRequestException( - response.code(), content, problem, submitTransactionAsyncProblem); - } else { - // Codes in the 5xx range indicate an error with the Horizon server. - throw new BadResponseException( - response.code(), content, problem, submitTransactionAsyncProblem); - } } - throw new UnknownResponseException(response.code(), content); - } finally { - response.close(); + if (response.getStatusCode() == 504) { + throw new RequestTimeoutException(response.getStatusCode(), content, problem); + } else if (response.getStatusCode() < 500) { + // Codes in the 4xx range indicate an error that failed given the information provided + throw new BadRequestException( + response.getStatusCode(), content, problem, submitTransactionAsyncProblem); + } else { + // Codes in the 5xx range indicate an error with the Horizon server. + throw new BadResponseException( + response.getStatusCode(), content, problem, submitTransactionAsyncProblem); + } } + + throw new UnknownResponseException(response.getStatusCode(), content); } } diff --git a/src/main/java/org/stellar/sdk/requests/RootRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/RootRequestBuilder.java index 2ac6c2d3a..656b3b2f4 100644 --- a/src/main/java/org/stellar/sdk/requests/RootRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/RootRequestBuilder.java @@ -1,15 +1,16 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.RootResponse; /** Builds requests connected to root. */ public class RootRequestBuilder extends RequestBuilder { - public RootRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "/"); + public RootRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "/"); } /** diff --git a/src/main/java/org/stellar/sdk/requests/SSEStream.java b/src/main/java/org/stellar/sdk/requests/SSEStream.java index 934f9d712..7ecfa65f8 100644 --- a/src/main/java/org/stellar/sdk/requests/SSEStream.java +++ b/src/main/java/org/stellar/sdk/requests/SSEStream.java @@ -1,31 +1,23 @@ package org.stellar.sdk.requests; import java.io.Closeable; -import java.net.SocketException; -import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.internal.sse.RealEventSource; -import okhttp3.sse.EventSource; -import okhttp3.sse.EventSourceListener; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; +import org.stellar.sdk.UriBuilder; +import org.stellar.sdk.UriUtil; import org.stellar.sdk.Util; -import org.stellar.sdk.responses.Pageable; -import org.stellar.sdk.responses.gson.GsonSingleton; +import org.stellar.sdk.http.sse.ISseClient; +import org.stellar.sdk.http.sse.ISseEventStream; +import org.stellar.sdk.http.sse.SseContext; public class SSEStream implements Closeable { static final long DEFAULT_RECONNECT_TIMEOUT = 15 * 1000L; - private final OkHttpClient okHttpClient; + private final ISseClient sseClient; private final RequestBuilder requestBuilder; private final Class responseClass; private final EventListener listener; @@ -34,18 +26,17 @@ public class SSEStream implements private final AtomicLong latestEventTime = new AtomicLong(0); private final AtomicReference lastEventId = new AtomicReference<>(null); private final ScheduledExecutorService executorService; - private final AtomicReference eventSource = new AtomicReference<>(null); + private final AtomicReference eventSource = new AtomicReference<>(null); private final long reconnectTimeout; private final AtomicLong currentListenerId = new AtomicLong(0); private SSEStream( - final OkHttpClient okHttpClient, + final ISseClient sseClient, final RequestBuilder requestBuilder, final Class responseClass, final EventListener listener, final long reconnectTimeout) { - // Create a new client with no read timeout - this.okHttpClient = okHttpClient.newBuilder().readTimeout(0, TimeUnit.MILLISECONDS).build(); + this.sseClient = sseClient; this.requestBuilder = requestBuilder; this.responseClass = responseClass; this.listener = listener; @@ -84,177 +75,87 @@ public String lastPagingToken() { } private void restart() { - EventSource currentEventSource = eventSource.get(); + final var currentEventSource = eventSource.get(); + if (currentEventSource != null) { currentEventSource.cancel(); } + // we cancelled the current event source, the `lastEventId` will not change anymore, // so we can safely restart with the same `lastEventId` long newListenerId = currentListenerId.incrementAndGet(); eventSource.set( doStreamRequest( this, - okHttpClient, + sseClient, requestBuilder, responseClass, listener, requestBuilder.uriBuilder.build().toString(), - source -> isClosed.compareAndSet(false, true), newListenerId)); } @Override public void close() { if (isStopped.compareAndSet(false, true)) { - EventSource currentEventSource = eventSource.get(); + ISseEventStream currentEventSource = eventSource.get(); + if (currentEventSource != null) { currentEventSource.cancel(); } + executorService.shutdownNow(); } } static SSEStream create( - OkHttpClient okHttpClient, + ISseClient sseClient, RequestBuilder requestBuilder, Class responseClass, EventListener listener, long reconnectTimeout) { SSEStream stream = - new SSEStream<>(okHttpClient, requestBuilder, responseClass, listener, reconnectTimeout); + new SSEStream<>(sseClient, requestBuilder, responseClass, listener, reconnectTimeout); stream.start(); return stream; } private static String addIdentificationQueryParameter(String url) { - HttpUrl parsedUrl = HttpUrl.parse(url); - if (parsedUrl == null) { + UriBuilder parsedUrl; + + try { + parsedUrl = new UriBuilder(url); + } catch (Exception e) { throw new IllegalArgumentException("Invalid URL: " + url); } - return parsedUrl - .newBuilder() - .addQueryParameter("X-Client-Name", "java-stellar-sdk") - .addQueryParameter("X-Client-Version", Util.getSdkVersion()) - .build() - .toString(); + parsedUrl.setQueryParameter("X-Client-Name", "java-stellar-sdk"); + parsedUrl.setQueryParameter("X-Client-Version", Util.getSdkVersion()); + return parsedUrl.build().toString(); } - private static EventSource doStreamRequest( + private static ISseEventStream doStreamRequest( final SSEStream stream, - final OkHttpClient okHttpClient, + final ISseClient sseClient, final RequestBuilder requestBuilder, final Class responseClass, final EventListener listener, String url, - final CloseListener closeListener, long listenerId) { + final var lastEventId = stream.lastEventId.get(); - Request.Builder builder = - new Request.Builder() - .url(addIdentificationQueryParameter(url)) - .header("Accept", "text/event-stream"); - String lastEventId = stream.lastEventId.get(); - if (lastEventId != null) { - builder.header("Last-Event-ID", lastEventId); - } - Request request = builder.build(); - RealEventSource eventSource = - new RealEventSource( - request, - new StellarEventSourceListener<>( - stream, closeListener, responseClass, requestBuilder, listener, listenerId)); - eventSource.connect(okHttpClient); - return eventSource; - } - - private interface CloseListener { - void closed(EventSource source); - } - - private static class StellarEventSourceListener - extends EventSourceListener { - private final SSEStream stream; - private final CloseListener closeListener; - private final Class responseClass; - private final RequestBuilder requestBuilder; - private final EventListener listener; - private final long listenerId; + final var context = + SseContext.builder() + .lastEventTime(stream.latestEventTime) + .currentListenerId(stream.currentListenerId) + .isClosed(stream.isClosed) + .isStopped(stream.isStopped) + .lastEventId(stream.lastEventId) + .build(); - StellarEventSourceListener( - SSEStream stream, - CloseListener closeListener, - Class responseClass, - RequestBuilder requestBuilder, - EventListener listener, - long listenerId) { - this.stream = stream; - this.closeListener = closeListener; - this.responseClass = responseClass; - this.requestBuilder = requestBuilder; - this.listener = listener; - this.listenerId = listenerId; - } + final var uri = UriUtil.toUri(addIdentificationQueryParameter(url)); - @Override - public void onClosed(@NotNull EventSource eventSource) { - if (stream.isStopped.get() || listenerId != stream.currentListenerId.get()) { - return; - } - if (closeListener != null) { - closeListener.closed(eventSource); - } - } - - @Override - public void onOpen(@NotNull EventSource eventSource, @NotNull Response response) {} - - @Override - public void onFailure( - @NotNull EventSource eventSource, @Nullable Throwable t, @Nullable Response response) { - if (stream.isStopped.get() || listenerId != stream.currentListenerId.get()) { - return; - } - Optional code = Optional.empty(); - if (response != null) { - code = Optional.of(response.code()); - } - if (t != null) { - if (t instanceof SocketException) { - if (closeListener != null) { - closeListener.closed(eventSource); - } - } else { - listener.onFailure(Optional.of(t), code); - } - } else { - Optional absent = Optional.empty(); - listener.onFailure(absent, code); - } - } - - @Override - public void onEvent( - @NotNull EventSource eventSource, - @Nullable String id, - @Nullable String type, - @NotNull String data) { - if (stream.isStopped.get() || listenerId != stream.currentListenerId.get()) { - return; - } - // Update the timestamp of the last received event. - stream.latestEventTime.set(System.currentTimeMillis()); - - if (data.equals("\"hello\"") || data.equals("\"byebye\"")) { - return; - } - T event = GsonSingleton.getInstance().fromJson(data, responseClass); - if (event instanceof Pageable) { - String pagingToken = ((Pageable) event).getPagingToken(); - requestBuilder.cursor(pagingToken); - } - stream.lastEventId.set(id); - listener.onEvent(event); - } + return sseClient.createEventStream( + context, responseClass, listenerId, listener, requestBuilder, uri, lastEventId); } } diff --git a/src/main/java/org/stellar/sdk/requests/StrictReceivePathsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/StrictReceivePathsRequestBuilder.java index 27bf5b68d..d2591c1f6 100644 --- a/src/main/java/org/stellar/sdk/requests/StrictReceivePathsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/StrictReceivePathsRequestBuilder.java @@ -1,19 +1,21 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; +import java.net.URI; import java.util.List; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import org.stellar.sdk.Asset; import org.stellar.sdk.AssetTypeCreditAlphaNum; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.PathResponse; /** Builds requests connected to paths. */ public class StrictReceivePathsRequestBuilder extends RequestBuilder { - public StrictReceivePathsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, ""); + public StrictReceivePathsRequestBuilder( + IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, ""); this.setSegments("paths", "strict-receive"); } @@ -23,7 +25,7 @@ public StrictReceivePathsRequestBuilder destinationAccount(String account) { } public StrictReceivePathsRequestBuilder sourceAccount(String account) { - if (uriBuilder.build().queryParameter("source_assets") != null) { + if (uriBuilder.hasQueryParameter("source_assets")) { throw new IllegalArgumentException("cannot set both source_assets and source_account"); } uriBuilder.setQueryParameter("source_account", account); @@ -31,7 +33,7 @@ public StrictReceivePathsRequestBuilder sourceAccount(String account) { } public StrictReceivePathsRequestBuilder sourceAssets(List assets) { - if (uriBuilder.build().queryParameter("source_account") != null) { + if (uriBuilder.hasQueryParameter("source_account")) { throw new IllegalArgumentException("cannot set both source_assets and source_account"); } setAssetsParameter("source_assets", assets); @@ -56,8 +58,8 @@ public StrictReceivePathsRequestBuilder destinationAsset(Asset asset) { /** * Requests specific uri and returns {@link Page} of {@link PathResponse}. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link PathResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -74,7 +76,7 @@ public StrictReceivePathsRequestBuilder destinationAsset(Asset asset) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } diff --git a/src/main/java/org/stellar/sdk/requests/StrictSendPathsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/StrictSendPathsRequestBuilder.java index dbeae87d0..bbf8be9c8 100644 --- a/src/main/java/org/stellar/sdk/requests/StrictSendPathsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/StrictSendPathsRequestBuilder.java @@ -1,23 +1,25 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; +import java.net.URI; import java.util.List; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import org.stellar.sdk.Asset; import org.stellar.sdk.AssetTypeCreditAlphaNum; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.PathResponse; public class StrictSendPathsRequestBuilder extends RequestBuilder { - public StrictSendPathsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, ""); + public StrictSendPathsRequestBuilder( + IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, ""); this.setSegments("paths", "strict-send"); } public StrictSendPathsRequestBuilder destinationAccount(String account) { - if (uriBuilder.build().queryParameter("destination_assets") != null) { + if (uriBuilder.hasQueryParameter("destination_assets")) { throw new IllegalArgumentException( "cannot set both destination_assets and destination_account"); } @@ -26,7 +28,7 @@ public StrictSendPathsRequestBuilder destinationAccount(String account) { } public StrictSendPathsRequestBuilder destinationAssets(List assets) { - if (uriBuilder.build().queryParameter("destination_account") != null) { + if (uriBuilder.hasQueryParameter("destination_account")) { throw new IllegalArgumentException( "cannot set both destination_assets and destination_account"); } @@ -52,8 +54,8 @@ public StrictSendPathsRequestBuilder sourceAsset(Asset asset) { /** * Requests specific uri and returns {@link Page} of {@link PathResponse}. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link PathResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -70,7 +72,7 @@ public StrictSendPathsRequestBuilder sourceAsset(Asset asset) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, ISseClient sseClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -95,6 +97,6 @@ public static Page execute(OkHttpClient httpClient, HttpUrl uri) { * due to cancellation or connectivity problems, etc. */ public Page execute() { - return execute(this.httpClient, this.buildUri()); + return execute(this.httpClient, this.sseClient, this.buildUri()); } } diff --git a/src/main/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilder.java index 6acb569ff..a19daca7e 100644 --- a/src/main/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilder.java @@ -1,26 +1,28 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; +import java.net.URI; import org.stellar.sdk.Asset; import org.stellar.sdk.AssetTypeCreditAlphaNum; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.TradeAggregationResponse; /** Builds requests connected to trades. */ public class TradeAggregationsRequestBuilder extends RequestBuilder { public TradeAggregationsRequestBuilder( - OkHttpClient httpClient, - HttpUrl serverURI, + IHttpClient httpClient, + ISseClient sseClient, + URI serverURI, Asset baseAsset, Asset counterAsset, long startTime, long endTime, long resolution, long offset) { - super(httpClient, serverURI, "trade_aggregations"); + super(httpClient, sseClient, serverURI, "trade_aggregations"); this.baseAsset(baseAsset); this.counterAsset(counterAsset); @@ -52,8 +54,8 @@ private void counterAsset(Asset asset) { * Requests specific uri and returns {@link Page} of {@link * TradeAggregationResponse}. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link TradeAggregationResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -70,7 +72,7 @@ private void counterAsset(Asset asset) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); diff --git a/src/main/java/org/stellar/sdk/requests/TradesRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/TradesRequestBuilder.java index 93b89475e..49a5e8362 100644 --- a/src/main/java/org/stellar/sdk/requests/TradesRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/TradesRequestBuilder.java @@ -1,12 +1,13 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; +import java.net.URI; import lombok.NonNull; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import org.stellar.sdk.Asset; import org.stellar.sdk.AssetTypeCreditAlphaNum; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.TradeResponse; @@ -14,8 +15,8 @@ public class TradesRequestBuilder extends RequestBuilder { private static final String TRADE_TYPE_PARAMETER_NAME = "trade_type"; - public TradesRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "trades"); + public TradesRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "trades"); } public TradesRequestBuilder baseAsset(Asset asset) { @@ -77,8 +78,8 @@ public TradesRequestBuilder forTradeType(@NonNull String tradeType) { /** * Requests specific uri and returns {@link Page} of {@link TradeResponse}. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link TradeResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -95,7 +96,7 @@ public TradesRequestBuilder forTradeType(@NonNull String tradeType) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -158,7 +159,7 @@ public TradesRequestBuilder limit(int number) { */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create(httpClient, this, TradeResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, TradeResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/requests/TransactionsRequestBuilder.java b/src/main/java/org/stellar/sdk/requests/TransactionsRequestBuilder.java index f9259fd57..d9a62668a 100644 --- a/src/main/java/org/stellar/sdk/requests/TransactionsRequestBuilder.java +++ b/src/main/java/org/stellar/sdk/requests/TransactionsRequestBuilder.java @@ -1,17 +1,18 @@ package org.stellar.sdk.requests; import com.google.gson.reflect.TypeToken; +import java.net.URI; import lombok.NonNull; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.sse.ISseClient; import org.stellar.sdk.responses.Page; import org.stellar.sdk.responses.TransactionResponse; /** Builds requests connected to transactions. */ public class TransactionsRequestBuilder extends RequestBuilder { - public TransactionsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { - super(httpClient, serverURI, "transactions"); + public TransactionsRequestBuilder(IHttpClient httpClient, ISseClient sseClient, URI serverURI) { + super(httpClient, sseClient, serverURI, "transactions"); } /** @@ -34,7 +35,7 @@ public TransactionsRequestBuilder(OkHttpClient httpClient, HttpUrl serverURI) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public TransactionResponse transaction(HttpUrl uri) { + public TransactionResponse transaction(URI uri) { TypeToken type = new TypeToken() {}; return executeGetRequest(httpClient, uri, type); } @@ -130,8 +131,8 @@ public TransactionsRequestBuilder includeFailed(boolean value) { /** * Requests specific uri and returns {@link Page} of {@link TransactionResponse}. * - * @param httpClient {@link OkHttpClient} to use to send the request. - * @param uri {@link HttpUrl} URI to send the request to. + * @param httpClient {@link IHttpClient} to use to send the request. + * @param uri {@link URI} URI to send the request to. * @return {@link Page} of {@link TransactionResponse} * @throws org.stellar.sdk.exception.NetworkException All the exceptions below are subclasses of * NetworkError @@ -148,7 +149,7 @@ public TransactionsRequestBuilder includeFailed(boolean value) { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public static Page execute(OkHttpClient httpClient, HttpUrl uri) { + public static Page execute(IHttpClient httpClient, URI uri) { TypeToken> type = new TypeToken>() {}; return executeGetRequest(httpClient, uri, type); } @@ -167,8 +168,7 @@ public static Page execute(OkHttpClient httpClient, HttpUrl */ public SSEStream stream( final EventListener listener, long reconnectTimeout) { - return SSEStream.create( - httpClient, this, TransactionResponse.class, listener, reconnectTimeout); + return SSEStream.create(sseClient, this, TransactionResponse.class, listener, reconnectTimeout); } /** diff --git a/src/main/java/org/stellar/sdk/responses/Page.java b/src/main/java/org/stellar/sdk/responses/Page.java index 73718b112..17fb4e078 100644 --- a/src/main/java/org/stellar/sdk/responses/Page.java +++ b/src/main/java/org/stellar/sdk/responses/Page.java @@ -4,14 +4,18 @@ import com.google.gson.reflect.TypeToken; import java.io.IOException; import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; import java.util.List; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.Value; -import okhttp3.OkHttpClient; -import okhttp3.Request; import org.stellar.sdk.exception.ConnectionErrorException; import org.stellar.sdk.exception.RequestTimeoutException; +import org.stellar.sdk.exception.UnexpectedException; +import org.stellar.sdk.http.GetRequest; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.StringResponse; import org.stellar.sdk.requests.ResponseHandler; import org.stellar.sdk.responses.gson.TypedResponse; @@ -52,7 +56,7 @@ public class Page extends Response implements TypedResponse> { * @throws org.stellar.sdk.exception.ConnectionErrorException When the request cannot be executed * due to cancellation or connectivity problems, etc. */ - public Page getNextPage(OkHttpClient httpClient) { + public Page getNextPage(IHttpClient httpClient) { if (this.getLinks().getNext() == null) { return null; } @@ -65,14 +69,16 @@ public Page getNextPage(OkHttpClient httpClient) { ResponseHandler> responseHandler = new ResponseHandler>(this.type); String url = this.getLinks().getNext().getHref(); - Request request = new Request.Builder().get().url(url).build(); - okhttp3.Response response; + StringResponse response; try { - response = httpClient.newCall(request).execute(); + final var request = new GetRequest(new URI(url)); + response = httpClient.get(request); } catch (SocketTimeoutException e) { throw new RequestTimeoutException(e); } catch (IOException e) { throw new ConnectionErrorException(e); + } catch (URISyntaxException e) { + throw new UnexpectedException(e); } return responseHandler.handleResponse(response); } diff --git a/src/test/java/org/stellar/sdk/ServerTest.java b/src/test/java/org/stellar/sdk/ServerTest.java index 33c0f02e7..ee994c03a 100644 --- a/src/test/java/org/stellar/sdk/ServerTest.java +++ b/src/test/java/org/stellar/sdk/ServerTest.java @@ -6,9 +6,9 @@ import java.io.IOException; import java.math.BigDecimal; import java.net.URISyntaxException; +import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import okhttp3.mockwebserver.Dispatcher; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; @@ -18,6 +18,7 @@ import org.stellar.sdk.exception.BadRequestException; import org.stellar.sdk.exception.BadResponseException; import org.stellar.sdk.exception.RequestTimeoutException; +import org.stellar.sdk.http.Jdk11HttpClient; import org.stellar.sdk.operations.AccountMergeOperation; import org.stellar.sdk.operations.CreateAccountOperation; import org.stellar.sdk.operations.ManageDataOperation; @@ -244,12 +245,12 @@ public void testSubmitTransactionTimeout() throws IOException { HttpUrl baseUrl = mockWebServer.url(""); Server server = new Server(baseUrl.toString()); - // We're creating a new OkHttpClient to make this test faster - OkHttpClient testSubmitHttpClient = - new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .retryOnConnectionFailure(false) + // We're creating a new IHttpClient to make this test faster + final var testSubmitHttpClient = + new Jdk11HttpClient.Builder() + .withConnectTimeout(10, ChronoUnit.SECONDS) + .withReadTimeout(10, ChronoUnit.SECONDS) + .withRetryOnConnectionFailure(false) .build(); server.setSubmitHttpClient(testSubmitHttpClient); Transaction tx = this.buildTransaction(); @@ -274,12 +275,12 @@ public void testSubmitTransactionTimeoutWithoutResponse() throws IOException { HttpUrl baseUrl = mockWebServer.url(""); Server server = new Server(baseUrl.toString()); - // We're creating a new OkHttpClient to make this test faster - OkHttpClient testSubmitHttpClient = - new OkHttpClient.Builder() - .connectTimeout(1, TimeUnit.SECONDS) - .readTimeout(1, TimeUnit.SECONDS) - .retryOnConnectionFailure(false) + // We're creating a new IHttpClient to make this test faster + final var testSubmitHttpClient = + new Jdk11HttpClient.Builder() + .withConnectTimeout(1, ChronoUnit.SECONDS) + .withReadTimeout(1, ChronoUnit.SECONDS) + .withRetryOnConnectionFailure(false) .build(); server.setSubmitHttpClient(testSubmitHttpClient); @@ -434,12 +435,12 @@ public void testSubmitTransactionInternalError() throws IOException { mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); Server server = new Server(baseUrl.toString()); - // We're creating a new OkHttpClient to make this test faster - OkHttpClient testSubmitHttpClient = - new OkHttpClient.Builder() - .connectTimeout(10, TimeUnit.SECONDS) - .readTimeout(10, TimeUnit.SECONDS) - .retryOnConnectionFailure(false) + // We're creating a new IHttpClient to make this test faster + final var testSubmitHttpClient = + new Jdk11HttpClient.Builder() + .withConnectTimeout(10, ChronoUnit.SECONDS) + .withReadTimeout(10, ChronoUnit.SECONDS) + .withRetryOnConnectionFailure(false) .build(); server.setSubmitHttpClient(testSubmitHttpClient); diff --git a/src/test/java/org/stellar/sdk/SslCertificateUtils.java b/src/test/java/org/stellar/sdk/SslCertificateUtils.java index ea981034c..a90a4f6a1 100644 --- a/src/test/java/org/stellar/sdk/SslCertificateUtils.java +++ b/src/test/java/org/stellar/sdk/SslCertificateUtils.java @@ -11,7 +11,6 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.bouncycastle.asn1.x500.X500Name; @@ -22,7 +21,7 @@ import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; public class SslCertificateUtils { - public static SSLSocketFactory createSslSocketFactory() { + public static SSLContext createSslContext() { try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); @@ -43,8 +42,7 @@ public static SSLSocketFactory createSslSocketFactory() { SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, new TrustManager[] {trustManager}, null); - - return sslContext.getSocketFactory(); + return sslContext; } catch (Exception e) { throw new RuntimeException("Failed to create SSL socket factory", e); } diff --git a/src/test/java/org/stellar/sdk/federation/FederationTest.java b/src/test/java/org/stellar/sdk/federation/FederationTest.java index 6d32beca4..be298e9f8 100644 --- a/src/test/java/org/stellar/sdk/federation/FederationTest.java +++ b/src/test/java/org/stellar/sdk/federation/FederationTest.java @@ -6,31 +6,28 @@ import java.io.IOException; import java.net.URLEncoder; -import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.SSLContext; import javax.net.ssl.X509TrustManager; import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.Test; import org.stellar.sdk.SslCertificateUtils; +import org.stellar.sdk.http.IHttpClient; +import org.stellar.sdk.http.Jdk11HttpClient; public class FederationTest { @Test public void testResolveAddressSuccess() throws IOException, InterruptedException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = "FEDERATION_SERVER = \"https://" + domain + "/federation\""; String successResponse = @@ -63,18 +60,14 @@ public void testResolveAddressSuccess() throws IOException, InterruptedException @Test public void testResolveAddressSuccessWithMemo() throws IOException, InterruptedException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = "FEDERATION_SERVER = \"https://" + domain + "/federation\""; String successResponse = @@ -107,18 +100,14 @@ public void testResolveAddressSuccessWithMemo() throws IOException, InterruptedE @Test public void testResolveAddressNotFound() throws IOException, InterruptedException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = "FEDERATION_SERVER = \"https://" + domain + "/federation\""; String notFoundResponse = "{\"code\":\"not_found\",\"message\":\"Account not found\"}"; @@ -144,18 +133,14 @@ public void testResolveAddressNotFound() throws IOException, InterruptedExceptio @Test public void testResolveAccountIdSuccess() throws IOException, InterruptedException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = "FEDERATION_SERVER = \"https://" + domain + "/federation\""; String successResponse = @@ -189,18 +174,14 @@ public void testResolveAccountIdSuccess() throws IOException, InterruptedExcepti @Test public void testResolveAccountIdSuccessWithMemo() throws IOException, InterruptedException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = "FEDERATION_SERVER = \"https://" + domain + "/federation\""; String successResponse = @@ -235,18 +216,14 @@ public void testResolveAccountIdSuccessWithMemo() throws IOException, Interrupte @Test public void testResolveAccountIdNotFound() throws IOException, InterruptedException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = "FEDERATION_SERVER = \"https://" + domain + "/federation\""; String notFoundResponse = "{\"code\":\"not_found\",\"message\":\"Account not found\"}"; @@ -277,18 +254,14 @@ public void testResolveAccountIdNotFound() throws IOException, InterruptedExcept @Test public void testStellarTomlNotFoundInvalidExceptionThrows() throws IOException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); mockWebServer.enqueue(new MockResponse().setResponseCode(404).setBody("")); assertThrows( @@ -299,18 +272,14 @@ public void testStellarTomlNotFoundInvalidExceptionThrows() throws IOException { @Test public void testNoFederationServerExceptionThrows() throws IOException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = ""; mockWebServer.enqueue(new MockResponse().setResponseCode(200).setBody(stellarToml)); @@ -322,18 +291,14 @@ public void testNoFederationServerExceptionThrows() throws IOException { @Test public void testFederationServerInvalidExceptionThrows() throws IOException { - SSLSocketFactory sslSocketFactory = SslCertificateUtils.createSslSocketFactory(); + SSLContext sslContext = SslCertificateUtils.createSslContext(); X509TrustManager trustAllCerts = SslCertificateUtils.createTrustAllCertsManager(); MockWebServer mockWebServer = new MockWebServer(); - mockWebServer.useHttps(sslSocketFactory, false); + mockWebServer.useHttps(sslContext.getSocketFactory(), false); mockWebServer.start(); HttpUrl baseUrl = mockWebServer.url(""); String domain = String.format("%s:%d", baseUrl.host(), baseUrl.port()); - OkHttpClient client = - new OkHttpClient.Builder() - .sslSocketFactory(sslSocketFactory, trustAllCerts) - .hostnameVerifier((hostname, session) -> true) - .build(); + final var client = laxTlsClient(sslContext); String stellarToml = "FEDERATION_SERVER = \"http://" + domain + "/federation\""; @@ -353,7 +318,11 @@ public void testFederationServerInvalidExceptionThrows() throws IOException { public void testMalformedAddressExceptionThrows() { assertThrows( IllegalArgumentException.class, - () -> new Federation().resolveAddress("bob*stellar.org*test")); - assertThrows(IllegalArgumentException.class, () -> new Federation().resolveAddress("bob")); + () -> new Federation(null).resolveAddress("bob*stellar.org*test")); + assertThrows(IllegalArgumentException.class, () -> new Federation(null).resolveAddress("bob")); + } + + private static IHttpClient laxTlsClient(SSLContext sslContext) { + return new Jdk11HttpClient.Builder().withSslContext(sslContext).build(); } } diff --git a/src/test/java/org/stellar/sdk/requests/AccountsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/AccountsRequestBuilderTest.java index 96b07d722..654c3c8f1 100644 --- a/src/test/java/org/stellar/sdk/requests/AccountsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/AccountsRequestBuilderTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.*; @@ -11,7 +10,7 @@ public class AccountsRequestBuilderTest { @Test public void testAccounts() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .accounts() .cursor("13537736921089") @@ -27,7 +26,7 @@ public void testAccounts() { @Test public void testForSigner() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .accounts() .forSigner("GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") @@ -41,7 +40,7 @@ public void testForSigner() { @Test public void testForSponsor() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .accounts() .forSponsor("GDSBCQO34HWPGUGQSP3QBFEXVTSR2PW46UIGTHVWGWJGQKH3AFNHXHXN") @@ -58,7 +57,7 @@ public void testForCreditAlphanum4Asset() { AssetTypeCreditAlphaNum asset = new AssetTypeCreditAlphaNum4( "USD", "GDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG"); - HttpUrl uri = server.accounts().forAsset(asset).buildUri(); + final var uri = server.accounts().forAsset(asset).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/accounts?asset=USD%3AGDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG", uri.toString()); @@ -71,7 +70,7 @@ public void testForCreditAlphanum12Asset() { AssetTypeCreditAlphaNum asset = new AssetTypeCreditAlphaNum12( "STELLAR", "GDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG"); - HttpUrl uri = server.accounts().forAsset(asset).buildUri(); + final var uri = server.accounts().forAsset(asset).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/accounts?asset=STELLAR%3AGDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG", uri.toString()); @@ -136,7 +135,7 @@ public void testForSignerAndSponsorInvalid() { public void testForLiquidityPool() { Server server = new Server("https://horizon-testnet.stellar.org"); String liquidityPoolId = "dd7b1ab831c273310ddbec6f97870aa83c2fbd78ce22aded37ecbf4f3380faca"; - HttpUrl uri = server.accounts().forLiquidityPool(liquidityPoolId).buildUri(); + final var uri = server.accounts().forLiquidityPool(liquidityPoolId).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/accounts?liquidity_pool=dd7b1ab831c273310ddbec6f97870aa83c2fbd78ce22aded37ecbf4f3380faca", uri.toString()); diff --git a/src/test/java/org/stellar/sdk/requests/AssetsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/AssetsRequestBuilderTest.java index f1e4eba48..0bdd21102 100644 --- a/src/test/java/org/stellar/sdk/requests/AssetsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/AssetsRequestBuilderTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.Server; @@ -10,7 +9,7 @@ public class AssetsRequestBuilderTest { @Test public void testAssets() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .assets() .assetCode("USD") diff --git a/src/test/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilderTest.java index 2a1a81d88..d0fdc3bb0 100644 --- a/src/test/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/ClaimableBalancesRequestBuilderTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; +import java.net.URI; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -16,7 +16,7 @@ public class ClaimableBalancesRequestBuilderTest { @Test public void testForSponsor() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .claimableBalances() .forSponsor("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -32,7 +32,7 @@ public void testForSponsor() { @Test public void testWithoutParams() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.claimableBalances().buildUri(); + URI uri = server.claimableBalances().buildUri(); assertEquals("https://horizon-testnet.stellar.org/claimable_balances", uri.toString()); server.close(); } @@ -40,7 +40,7 @@ public void testWithoutParams() { @Test public void testForClaimant() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .claimableBalances() .forClaimant("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -54,7 +54,7 @@ public void testForClaimant() { @Test public void testForNativeAsset() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.claimableBalances().forAsset(Asset.create("native")).buildUri(); + URI uri = server.claimableBalances().forAsset(Asset.create("native")).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/claimable_balances?asset=native", uri.toString()); server.close(); @@ -63,7 +63,7 @@ public void testForNativeAsset() { @Test public void testForAsset() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .claimableBalances() .forAsset(Asset.create("USD:GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H")) @@ -77,7 +77,7 @@ public void testForAsset() { @Test public void testForAssetAndClaimant() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .claimableBalances() .forClaimant("GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY") diff --git a/src/test/java/org/stellar/sdk/requests/InterceptorTest.java b/src/test/java/org/stellar/sdk/requests/ClientIdHeadersTest.java similarity index 67% rename from src/test/java/org/stellar/sdk/requests/InterceptorTest.java rename to src/test/java/org/stellar/sdk/requests/ClientIdHeadersTest.java index b62db545f..9025d7c7b 100644 --- a/src/test/java/org/stellar/sdk/requests/InterceptorTest.java +++ b/src/test/java/org/stellar/sdk/requests/ClientIdHeadersTest.java @@ -3,26 +3,27 @@ import static org.junit.Assert.assertEquals; import java.io.IOException; -import okhttp3.OkHttpClient; -import okhttp3.Request; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.Test; +import org.stellar.sdk.http.GetRequest; +import org.stellar.sdk.http.Jdk11HttpClient; -public class InterceptorTest { +public class ClientIdHeadersTest { @Test public void testClientIdentificationInterceptor() throws IOException, InterruptedException { MockWebServer mockWebServer = new MockWebServer(); mockWebServer.start(); mockWebServer.enqueue(new MockResponse()); - OkHttpClient okHttpClient = - new OkHttpClient() - .newBuilder() - .addInterceptor(new ClientIdentificationInterceptor()) + final var httpClient = + new Jdk11HttpClient.Builder() + .withDefaultHeader("X-Client-Name", "java-stellar-sdk") + .withDefaultHeader("X-Client-Version", "dev") .build(); - okHttpClient.newCall(new Request.Builder().url(mockWebServer.url("/")).build()).execute(); + + httpClient.get(new GetRequest(mockWebServer.url("/").uri())); RecordedRequest request = mockWebServer.takeRequest(); assertEquals("java-stellar-sdk", request.getHeader("X-Client-Name")); diff --git a/src/test/java/org/stellar/sdk/requests/EffectsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/EffectsRequestBuilderTest.java index 195266bb6..1d9330ca8 100644 --- a/src/test/java/org/stellar/sdk/requests/EffectsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/EffectsRequestBuilderTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.Server; @@ -10,7 +9,7 @@ public class EffectsRequestBuilderTest { @Test public void testEffects() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.effects().limit(200).order(RequestBuilder.Order.DESC).buildUri(); + final var uri = server.effects().limit(200).order(RequestBuilder.Order.DESC).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/effects?limit=200&order=desc", uri.toString()); server.close(); @@ -19,7 +18,7 @@ public void testEffects() { @Test public void testForAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .effects() .forAccount("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -35,7 +34,7 @@ public void testForAccount() { @Test public void testForLedger() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .effects() .forLedger(200000000000L) @@ -51,7 +50,7 @@ public void testForLedger() { @Test public void testForLiquidityPool() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .effects() .forLiquidityPool("67260c4c1807b262ff851b0a3fe141194936bb0215b2f77447f1df11998eabb9") @@ -65,7 +64,7 @@ public void testForLiquidityPool() { @Test public void testForTransaction() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .effects() .forTransaction("991534d902063b7715cd74207bef4e7bd7aa2f108f62d3eba837ce6023b2d4f3") @@ -79,7 +78,7 @@ public void testForTransaction() { @Test public void testForOperation() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.effects().forOperation(28798257847L).cursor("85794837").buildUri(); + final var uri = server.effects().forOperation(28798257847L).cursor("85794837").buildUri(); assertEquals( "https://horizon-testnet.stellar.org/operations/28798257847/effects?cursor=85794837", uri.toString()); diff --git a/src/test/java/org/stellar/sdk/requests/FeeRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/FeeRequestBuilderTest.java index 4e25942ff..404cb432a 100644 --- a/src/test/java/org/stellar/sdk/requests/FeeRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/FeeRequestBuilderTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.Server; @@ -10,7 +9,7 @@ public class FeeRequestBuilderTest { @Test public void testBuilder() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.feeStats().buildUri(); + final var uri = server.feeStats().buildUri(); assertEquals("https://horizon-testnet.stellar.org/fee_stats", uri.toString()); server.close(); } diff --git a/src/test/java/org/stellar/sdk/requests/LedgersRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/LedgersRequestBuilderTest.java index 7f998318e..945ac71c8 100644 --- a/src/test/java/org/stellar/sdk/requests/LedgersRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/LedgersRequestBuilderTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; +import java.net.URI; import org.junit.Test; import org.stellar.sdk.Server; @@ -10,7 +10,7 @@ public class LedgersRequestBuilderTest { @Test public void testLedgers() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.ledgers().limit(200).order(RequestBuilder.Order.ASC).buildUri(); + URI uri = server.ledgers().limit(200).order(RequestBuilder.Order.ASC).buildUri(); assertEquals("https://horizon-testnet.stellar.org/ledgers?limit=200&order=asc", uri.toString()); server.close(); } diff --git a/src/test/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilderTest.java index a397b2b4f..6716df8f8 100644 --- a/src/test/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/LiquidityPoolsRequestBuilderTest.java @@ -2,7 +2,7 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; +import java.net.URI; import org.junit.Test; import org.stellar.sdk.*; @@ -10,7 +10,7 @@ public class LiquidityPoolsRequestBuilderTest { @Test public void testLiquidityPools() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .liquidityPools() .cursor("13537736921089") @@ -26,7 +26,7 @@ public void testLiquidityPools() { @Test public void testForReserves() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .liquidityPools() .forReserves( @@ -42,7 +42,7 @@ public void testForReserves() { @Test public void testForAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .liquidityPools() .forAccount("GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S") @@ -56,7 +56,7 @@ public void testForAccount() { @Test public void testForAccountClear() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .liquidityPools() .forAccount("GAP5LETOV6YIE62YAM56STDANPRDO7ZFDBGSNHJQIYGGKSMOZAHOOS2S") diff --git a/src/test/java/org/stellar/sdk/requests/OffersRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/OffersRequestBuilderTest.java index ae56a3e78..eff722202 100644 --- a/src/test/java/org/stellar/sdk/requests/OffersRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/OffersRequestBuilderTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -44,7 +43,7 @@ public class OffersRequestBuilderTest { @Test public void testWithoutParams() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.offers().buildUri(); + final var uri = server.offers().buildUri(); assertEquals("https://horizon-testnet.stellar.org/offers", uri.toString()); server.close(); } @@ -52,7 +51,7 @@ public void testWithoutParams() { @Test public void testForSeller() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .offers() .forSeller("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -66,7 +65,7 @@ public void testForSeller() { @Test public void testForSponsor() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .offers() .forSponsor("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -83,7 +82,7 @@ public void testForSellingCreditAlphanum4Asset() { Asset selling = new AssetTypeCreditAlphaNum4( "USD", "GDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG"); - HttpUrl uri = server.offers().forSellingAsset(selling).buildUri(); + final var uri = server.offers().forSellingAsset(selling).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/offers?selling=USD%3AGDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG", uri.toString()); @@ -96,7 +95,7 @@ public void testForBuyingCreditAlphanum4Asset() { Asset buying = new AssetTypeCreditAlphaNum4( "XCN", "GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY"); - HttpUrl uri = server.offers().forBuyingAsset(buying).buildUri(); + final var uri = server.offers().forBuyingAsset(buying).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/offers?buying=XCN%3AGCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY", uri.toString()); @@ -109,7 +108,7 @@ public void testForSellingCreditAlphanum12Asset() { Asset selling = new AssetTypeCreditAlphaNum12( "STELLAR", "GDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG"); - HttpUrl uri = server.offers().forSellingAsset(selling).buildUri(); + final var uri = server.offers().forSellingAsset(selling).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/offers?selling=STELLAR%3AGDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG", uri.toString()); @@ -122,7 +121,7 @@ public void testForBuyingCreditAlphanum12Asset() { Asset buying = new AssetTypeCreditAlphaNum12( "STELLAR", "GDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG"); - HttpUrl uri = server.offers().forBuyingAsset(buying).buildUri(); + final var uri = server.offers().forBuyingAsset(buying).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/offers?buying=STELLAR%3AGDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG", uri.toString()); @@ -133,7 +132,7 @@ public void testForBuyingCreditAlphanum12Asset() { public void testForSellingNativeAsset() { Server server = new Server("https://horizon-testnet.stellar.org"); Asset selling = new AssetTypeNative(); - HttpUrl uri = server.offers().forSellingAsset(selling).buildUri(); + final var uri = server.offers().forSellingAsset(selling).buildUri(); assertEquals("https://horizon-testnet.stellar.org/offers?selling=native", uri.toString()); server.close(); } @@ -142,7 +141,7 @@ public void testForSellingNativeAsset() { public void testForBuyingNativeAsset() { Server server = new Server("https://horizon-testnet.stellar.org"); Asset buying = new AssetTypeNative(); - HttpUrl uri = server.offers().forBuyingAsset(buying).buildUri(); + final var uri = server.offers().forBuyingAsset(buying).buildUri(); assertEquals("https://horizon-testnet.stellar.org/offers?buying=native", uri.toString()); server.close(); } @@ -156,7 +155,7 @@ public void testForSellingAssetAndBuyingAsset() { Asset buying = new AssetTypeCreditAlphaNum4( "XCN", "GCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY"); - HttpUrl uri = server.offers().forSellingAsset(selling).forBuyingAsset(buying).buildUri(); + final var uri = server.offers().forSellingAsset(selling).forBuyingAsset(buying).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/offers?selling=USD%3AGDVDKQFP665JAO7A2LSHNLQIUNYNAAIGJ6FYJVMG4DT3YJQQJSRBLQDG&buying=XCN%3AGCNY5OXYSY4FKHOPT2SPOQZAOEIGXB5LBYW3HVU3OWSTQITS65M5RCNY", uri.toString()); diff --git a/src/test/java/org/stellar/sdk/requests/OperationsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/OperationsRequestBuilderTest.java index 42a686800..fc295a263 100644 --- a/src/test/java/org/stellar/sdk/requests/OperationsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/OperationsRequestBuilderTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import java.io.IOException; -import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; @@ -48,7 +47,7 @@ public class OperationsRequestBuilderTest { @Test public void testOperations() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.operations().limit(200).order(RequestBuilder.Order.DESC).buildUri(); + final var uri = server.operations().limit(200).order(RequestBuilder.Order.DESC).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/operations?limit=200&order=desc", uri.toString()); server.close(); @@ -61,20 +60,20 @@ public void testOperationById() throws IOException, InterruptedException { mockWebServer.start(); Server server = new Server(mockWebServer.url("").toString()); OperationResponse response = server.operations().operation(438086668289L); - assertEquals(response.getType(), "create_account"); - assertEquals(response.getId(), Long.valueOf(438086668289L)); + assertEquals("create_account", response.getType()); + assertEquals(Long.valueOf(438086668289L), response.getId()); assertEquals( - response.getSourceAccount(), "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H"); + "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H", response.getSourceAccount()); RecordedRequest request = mockWebServer.takeRequest(); - assertEquals(request.getMethod(), "GET"); - assertEquals(request.getPath(), "/operations/438086668289"); + assertEquals("GET", request.getMethod()); + assertEquals("/operations/438086668289", request.getPath()); server.close(); } @Test public void testForAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .operations() .forAccount("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -90,7 +89,7 @@ public void testForAccount() { @Test public void testForClaimableBalance() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .operations() .forClaimableBalance( @@ -107,7 +106,7 @@ public void testForClaimableBalance() { @Test public void testForLedger() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .operations() .forLedger(200000000000L) @@ -123,7 +122,7 @@ public void testForLedger() { @Test public void testForLiquidityPool() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .operations() .forLiquidityPool("67260c4c1807b262ff851b0a3fe141194936bb0215b2f77447f1df11998eabb9") @@ -137,7 +136,7 @@ public void testForLiquidityPool() { @Test public void testForTransaction() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .operations() .forTransaction("991534d902063b7715cd74207bef4e7bd7aa2f108f62d3eba837ce6023b2d4f3") @@ -151,7 +150,7 @@ public void testForTransaction() { @Test public void testIncludeFailed() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .operations() .forLedger(200000000000L) @@ -168,7 +167,7 @@ public void testIncludeFailed() { @Test public void testIncludeTransactions() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + var uri = server .operations() .forLedger(200000000000L) @@ -199,7 +198,7 @@ public void testIncludeTransactions() { @Test public void testExcludeTransactions() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .operations() .forLedger(200000000000L) diff --git a/src/test/java/org/stellar/sdk/requests/OrderBookRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/OrderBookRequestBuilderTest.java index 0887825e0..30a3d77e6 100644 --- a/src/test/java/org/stellar/sdk/requests/OrderBookRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/OrderBookRequestBuilderTest.java @@ -3,7 +3,7 @@ import static org.junit.Assert.assertEquals; import static org.stellar.sdk.Asset.createNonNativeAsset; -import okhttp3.HttpUrl; +import java.net.URI; import org.junit.Test; import org.stellar.sdk.Server; @@ -11,7 +11,7 @@ public class OrderBookRequestBuilderTest { @Test public void testOrderBook() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + URI uri = server .orderBook() .buyingAsset( diff --git a/src/test/java/org/stellar/sdk/requests/PathsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/PathsRequestBuilderTest.java index 844379835..db6362be0 100644 --- a/src/test/java/org/stellar/sdk/requests/PathsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/PathsRequestBuilderTest.java @@ -8,7 +8,6 @@ import java.util.Arrays; import java.util.List; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.Asset; import org.stellar.sdk.Server; @@ -17,7 +16,7 @@ public class PathsRequestBuilderTest { @Test public void testStrictReceiveWithSourceAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .strictReceivePaths() .destinationAccount("GB24QI3BJNKBY4YNJZ2I37HFIYK56BL2OURFML76X46RQQKDLVT7WKJF") @@ -55,7 +54,7 @@ public void testStrictReceiveWithSourceAssets() { createNonNativeAsset( "EUR", "GAYSHLG75RPSMXWJ5KX7O7STE6RSZTD6NE4CTWAXFZYYVYIFRUVJIBJH")); Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .strictReceivePaths() .destinationAccount("GB24QI3BJNKBY4YNJZ2I37HFIYK56BL2OURFML76X46RQQKDLVT7WKJF") @@ -119,7 +118,7 @@ public void testStrictReceiveWithSourceAccountAndSourceAssets() { @Test public void testStrictSendWithDestinationAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .strictSendPaths() .destinationAccount("GB24QI3BJNKBY4YNJZ2I37HFIYK56BL2OURFML76X46RQQKDLVT7WKJF") @@ -155,7 +154,7 @@ public void testStrictSendWithDestinationAssets() { createNonNativeAsset( "EUR", "GAYSHLG75RPSMXWJ5KX7O7STE6RSZTD6NE4CTWAXFZYYVYIFRUVJIBJH")); Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .strictSendPaths() .destinationAssets(assets) diff --git a/src/test/java/org/stellar/sdk/requests/PaymentsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/PaymentsRequestBuilderTest.java index f358b0a40..ba0f922ae 100644 --- a/src/test/java/org/stellar/sdk/requests/PaymentsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/PaymentsRequestBuilderTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.Server; @@ -10,7 +9,7 @@ public class PaymentsRequestBuilderTest { @Test public void testPayments() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.payments().limit(200).order(RequestBuilder.Order.DESC).buildUri(); + final var uri = server.payments().limit(200).order(RequestBuilder.Order.DESC).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/payments?limit=200&order=desc", uri.toString()); server.close(); @@ -19,7 +18,7 @@ public void testPayments() { @Test public void testForAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .payments() .forAccount("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -35,7 +34,7 @@ public void testForAccount() { @Test public void testForLedger() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .payments() .forLedger(200000000000L) @@ -51,7 +50,7 @@ public void testForLedger() { @Test public void testForTransaction() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .payments() .forTransaction("991534d902063b7715cd74207bef4e7bd7aa2f108f62d3eba837ce6023b2d4f3") @@ -65,7 +64,7 @@ public void testForTransaction() { @Test public void testIncludeTransactions() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + var uri = server .payments() .forTransaction("991534d902063b7715cd74207bef4e7bd7aa2f108f62d3eba837ce6023b2d4f3") @@ -93,7 +92,7 @@ public void testIncludeTransactions() { @Test public void testExcludeTransactions() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .payments() .forTransaction("991534d902063b7715cd74207bef4e7bd7aa2f108f62d3eba837ce6023b2d4f3") diff --git a/src/test/java/org/stellar/sdk/requests/ResponseHandlerTest.java b/src/test/java/org/stellar/sdk/requests/ResponseHandlerTest.java index 55f42c17f..ef8f64a9d 100644 --- a/src/test/java/org/stellar/sdk/requests/ResponseHandlerTest.java +++ b/src/test/java/org/stellar/sdk/requests/ResponseHandlerTest.java @@ -3,12 +3,12 @@ import static org.junit.Assert.assertEquals; import java.io.IOException; -import okhttp3.OkHttpClient; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import org.junit.Assert; import org.junit.Test; import org.stellar.sdk.exception.TooManyRequestsException; +import org.stellar.sdk.http.Jdk11HttpClient; public class ResponseHandlerTest { @@ -23,10 +23,9 @@ public void testTooManyRequests() throws IOException { mockWebServer.start(); mockWebServer.enqueue(response); - OkHttpClient okHttpClient = new OkHttpClient().newBuilder().build(); + final var httpClient = new Jdk11HttpClient.Builder().build(); try { - - AccountsRequestBuilder.execute(okHttpClient, mockWebServer.url("/")); + AccountsRequestBuilder.execute(httpClient, mockWebServer.url("/").uri()); Assert.fail(); } catch (TooManyRequestsException tmre) { assertEquals(10, tmre.getRetryAfter().intValue()); @@ -48,10 +47,10 @@ public void testTooManyRequestsNoHeader() throws IOException, InterruptedExcepti mockWebServer.start(); mockWebServer.enqueue(response); - OkHttpClient okHttpClient = new OkHttpClient().newBuilder().build(); + final var httpClient = new Jdk11HttpClient.Builder().build(); try { - AccountsRequestBuilder.execute(okHttpClient, mockWebServer.url("/")); + AccountsRequestBuilder.execute(httpClient, mockWebServer.url("/").uri()); Assert.fail(); } catch (TooManyRequestsException tmre) { assertEquals(null, tmre.getRetryAfter()); diff --git a/src/test/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilderTest.java index dfdd6bcf0..ecc71721b 100644 --- a/src/test/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/TradeAggregationsRequestBuilderTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.stellar.sdk.Asset.createNonNativeAsset; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.AssetTypeNative; import org.stellar.sdk.Server; @@ -12,7 +11,7 @@ public class TradeAggregationsRequestBuilderTest { @Test public void testTradeAggregations() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .tradeAggregations( new AssetTypeNative(), diff --git a/src/test/java/org/stellar/sdk/requests/TradesRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/TradesRequestBuilderTest.java index 0329a8d65..95cfd9c34 100644 --- a/src/test/java/org/stellar/sdk/requests/TradesRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/TradesRequestBuilderTest.java @@ -3,7 +3,6 @@ import static org.junit.Assert.assertEquals; import static org.stellar.sdk.Asset.createNonNativeAsset; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.Server; @@ -11,7 +10,7 @@ public class TradesRequestBuilderTest { @Test public void testTrades() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .trades() .baseAsset( @@ -45,7 +44,7 @@ public void testTrades() { @Test public void testTradesForAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .trades() .forAccount("GDRRHSJMHXDTQBT4JTCILNGF5AS54FEMTXL7KOLMF6TFTHRK6SSUSUZZ") @@ -66,7 +65,7 @@ public void testTradesForAccount() { @Test public void testForLiquidityPool() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .trades() .forLiquidityPool("67260c4c1807b262ff851b0a3fe141194936bb0215b2f77447f1df11998eabb9") @@ -80,7 +79,7 @@ public void testForLiquidityPool() { @Test public void testForNullOfferId() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .trades() .offerId(12345L) diff --git a/src/test/java/org/stellar/sdk/requests/TransactionsRequestBuilderTest.java b/src/test/java/org/stellar/sdk/requests/TransactionsRequestBuilderTest.java index a1b40d4cf..c8e9ae8e9 100644 --- a/src/test/java/org/stellar/sdk/requests/TransactionsRequestBuilderTest.java +++ b/src/test/java/org/stellar/sdk/requests/TransactionsRequestBuilderTest.java @@ -2,7 +2,6 @@ import static org.junit.Assert.assertEquals; -import okhttp3.HttpUrl; import org.junit.Test; import org.stellar.sdk.Server; @@ -10,7 +9,7 @@ public class TransactionsRequestBuilderTest { @Test public void testTransactions() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = server.transactions().limit(200).order(RequestBuilder.Order.DESC).buildUri(); + final var uri = server.transactions().limit(200).order(RequestBuilder.Order.DESC).buildUri(); assertEquals( "https://horizon-testnet.stellar.org/transactions?limit=200&order=desc", uri.toString()); server.close(); @@ -19,7 +18,7 @@ public void testTransactions() { @Test public void testForAccount() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .transactions() .forAccount("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H") @@ -35,7 +34,7 @@ public void testForAccount() { @Test public void testForClaimableBalance() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .transactions() .forClaimableBalance( @@ -52,7 +51,7 @@ public void testForClaimableBalance() { @Test public void testForLedger() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .transactions() .forLedger(200000000000L) @@ -68,7 +67,7 @@ public void testForLedger() { @Test public void testForLiquidityPool() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .transactions() .forLiquidityPool("67260c4c1807b262ff851b0a3fe141194936bb0215b2f77447f1df11998eabb9") @@ -82,7 +81,7 @@ public void testForLiquidityPool() { @Test public void testIncludeFailed() { Server server = new Server("https://horizon-testnet.stellar.org"); - HttpUrl uri = + final var uri = server .transactions() .forLedger(200000000000L) From 34b92d04ebc3edca9de302d4c577913056eb7131 Mon Sep 17 00:00:00 2001 From: Joe Cavazos Date: Fri, 7 Feb 2025 08:53:33 -0600 Subject: [PATCH 2/3] #496: Add test for CxfSseEventStream. --- .../sdk/http/sse/CxfSseEventStream.java | 12 +- .../sdk/http/sse/CxfSseEventStreamTest.java | 373 ++++++++++++++++++ .../sdk/http/sse/MockCxfEventSource.java | 60 +++ 3 files changed, 442 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/stellar/sdk/http/sse/CxfSseEventStreamTest.java create mode 100644 src/test/java/org/stellar/sdk/http/sse/MockCxfEventSource.java diff --git a/src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java b/src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java index c6ef2b506..3272cd8aa 100644 --- a/src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java +++ b/src/main/java/org/stellar/sdk/http/sse/CxfSseEventStream.java @@ -9,6 +9,12 @@ import org.stellar.sdk.responses.Pageable; import org.stellar.sdk.responses.gson.GsonSingleton; +/** + * SSE event handler backed by Apache CXF. + * + * @see https://cxf.apache.org/docs/sse.html + * @param the SDK response class. + */ @Getter public class CxfSseEventStream implements ISseEventStream { @@ -41,13 +47,13 @@ public CxfSseEventStream( context.getLastEventTime().set(System.currentTimeMillis()); - final var eventData = inboundSseEvent.readData(); + final var eventDataString = inboundSseEvent.readData(); - if (eventData.equals("\"hello\"") || eventData.equals("\"byebye\"")) { + if (eventDataString.equals("\"hello\"") || eventDataString.equals("\"byebye\"")) { return; } - T event = GsonSingleton.getInstance().fromJson(eventData, responseClass); + T event = GsonSingleton.getInstance().fromJson(eventDataString, responseClass); if (event instanceof Pageable) { String pagingToken = ((Pageable) event).getPagingToken(); diff --git a/src/test/java/org/stellar/sdk/http/sse/CxfSseEventStreamTest.java b/src/test/java/org/stellar/sdk/http/sse/CxfSseEventStreamTest.java new file mode 100644 index 000000000..bd4e026ff --- /dev/null +++ b/src/test/java/org/stellar/sdk/http/sse/CxfSseEventStreamTest.java @@ -0,0 +1,373 @@ +package org.stellar.sdk.http.sse; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import jakarta.ws.rs.sse.InboundSseEvent; +import java.net.SocketException; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.stellar.sdk.requests.EventListener; +import org.stellar.sdk.requests.RequestBuilder; +import org.stellar.sdk.responses.AccountResponse; +import org.stellar.sdk.responses.gson.GsonSingleton; + +@RunWith(MockitoJUnitRunner.class) +public class CxfSseEventStreamTest { + + @Mock private EventListener eventListener; + + @Mock private RequestBuilder requestBuilder; + + @Captor private ArgumentCaptor eventListenerCaptor; + + @Captor private ArgumentCaptor> eventListenerThrowableCaptor; + + @Captor private ArgumentCaptor> eventListenerErrorCodeCaptor; + + @Before + public void setup() { + doNothing().when(eventListener).onEvent(eventListenerCaptor.capture()); + doNothing() + .when(eventListener) + .onFailure(eventListenerThrowableCaptor.capture(), eventListenerErrorCodeCaptor.capture()); + } + + @Test + public void testEventStoppedContextDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(true); + + final var event = mock(InboundSseEvent.class); + mockEventSource.handleEvent(event); + + verifyNoInteractions(event); + verifyNoInteractions(eventListener); + } + } + + @Test + public void testEventChangedListenerIdDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getCurrentListenerId().set(45678L); + + final var event = mock(InboundSseEvent.class); + mockEventSource.handleEvent(event); + + verifyNoInteractions(event); + verifyNoInteractions(eventListener); + } + } + + @Test + public void testEventDataHelloDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getCurrentListenerId().set(listenerId); + + final var event = mock(InboundSseEvent.class); + when(event.readData()).thenReturn("\"hello\""); + mockEventSource.handleEvent(event); + + verify(event, times(1)).readData(); + verifyNoInteractions(eventListener); + } + } + + @Test + public void testEventDataByeByeDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getCurrentListenerId().set(listenerId); + + final var event = mock(InboundSseEvent.class); + when(event.readData()).thenReturn("\"byebye\""); + mockEventSource.handleEvent(event); + + verify(event, times(1)).readData(); + verifyNoInteractions(eventListener); + } + } + + @Test + public void testEventNormal() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getCurrentListenerId().set(listenerId); + + final var response = new AccountResponse(); + + final var event = mock(InboundSseEvent.class); + when(event.readData()).thenReturn(toJson(response)); + when(event.getId()).thenReturn("new event ID"); + mockEventSource.handleEvent(event); + + verify(event, times(1)).readData(); + + assertNotNull(eventListenerCaptor.getValue()); + assertEquals(response, eventListenerCaptor.getValue()); + } + } + + @Test + public void testErrorStoppedContextDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(true); + mockEventSource.handleError(new Exception("error")); + verifyNoInteractions(eventListener); + } + } + + @Test + public void testErrorChangedListenerIdDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getCurrentListenerId().set(45678L); + mockEventSource.handleError(new Exception("error")); + verifyNoInteractions(eventListener); + } + } + + @Test + public void testErrorSocketExceptionClosesContext() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getIsClosed().set(false); + context.getCurrentListenerId().set(listenerId); + mockEventSource.handleError(new SocketException()); + + assertTrue(context.getIsClosed().get()); + verifyNoInteractions(eventListener); + } + } + + @Test + public void testErrorHandlesThrowable() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getIsClosed().set(false); + context.getCurrentListenerId().set(listenerId); + final var error = new Exception(); + mockEventSource.handleError(error); + + assertFalse(context.getIsClosed().get()); + assertNotNull(eventListenerThrowableCaptor.getValue()); + assertTrue(eventListenerThrowableCaptor.getValue().isPresent()); + assertEquals(error, eventListenerThrowableCaptor.getValue().get()); + } + } + + @Test + public void testErrorHandlesNullThrowable() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getIsClosed().set(false); + context.getCurrentListenerId().set(listenerId); + mockEventSource.handleError(null); + + assertFalse(context.getIsClosed().get()); + assertNotNull(eventListenerThrowableCaptor.getValue()); + assertTrue(eventListenerThrowableCaptor.getValue().isEmpty()); + } + } + + @Test + public void testCompleteStoppedContextDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(true); + context.getIsClosed().set(false); + mockEventSource.complete(); + verifyNoInteractions(eventListener); + assertFalse(context.getIsClosed().get()); + } + } + + @Test + public void testCompleteChangedListenerIdDoesNothing() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getCurrentListenerId().set(45678L); + mockEventSource.complete(); + verifyNoInteractions(eventListener); + assertFalse(context.getIsClosed().get()); + } + } + + @Test + public void testCompleteClosesContext() { + final var context = mockContext(); + final var listenerId = 34567L; + try (final var mockEventSource = new MockCxfEventSource()) { + final var stream = + new CxfSseEventStream<>( + context, + mockEventSource, + AccountResponse.class, + listenerId, + eventListener, + requestBuilder); + + context.getIsStopped().set(false); + context.getIsClosed().set(false); + context.getCurrentListenerId().set(listenerId); + mockEventSource.complete(); + verifyNoInteractions(eventListener); + assertTrue(context.getIsClosed().get()); + } + } + + private static SseContext mockContext() { + return new SseContext( + new AtomicBoolean(), + new AtomicBoolean(), + new AtomicLong(), + new AtomicLong(), + new AtomicReference()); + } + + private static String toJson(Object object) { + return GsonSingleton.getInstance().toJson(object); + } +} diff --git a/src/test/java/org/stellar/sdk/http/sse/MockCxfEventSource.java b/src/test/java/org/stellar/sdk/http/sse/MockCxfEventSource.java new file mode 100644 index 000000000..59a38b575 --- /dev/null +++ b/src/test/java/org/stellar/sdk/http/sse/MockCxfEventSource.java @@ -0,0 +1,60 @@ +package org.stellar.sdk.http.sse; + +import jakarta.ws.rs.sse.InboundSseEvent; +import jakarta.ws.rs.sse.SseEventSource; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class MockCxfEventSource implements SseEventSource { + private Consumer eventConsumer; + private Consumer errorConsumer; + private Runnable completionInstruction; + private boolean open; + + @Override + public void register(Consumer consumer) { + this.eventConsumer = consumer; + } + + @Override + public void register(Consumer consumer, Consumer consumer1) { + this.eventConsumer = consumer; + this.errorConsumer = consumer1; + } + + @Override + public void register( + Consumer consumer, Consumer consumer1, Runnable runnable) { + this.eventConsumer = consumer; + this.errorConsumer = consumer1; + this.completionInstruction = runnable; + } + + @Override + public void open() { + this.open = true; + } + + @Override + public boolean isOpen() { + return open; + } + + @Override + public boolean close(long l, TimeUnit timeUnit) { + this.open = false; + return false; + } + + public void handleEvent(InboundSseEvent event) { + eventConsumer.accept(event); + } + + public void handleError(Throwable throwable) { + errorConsumer.accept(throwable); + } + + public void complete() { + completionInstruction.run(); + } +} From 012971e9ba23bee843120511f27ade48d2dba948 Mon Sep 17 00:00:00 2001 From: Joe Cavazos Date: Fri, 7 Feb 2025 10:05:29 -0600 Subject: [PATCH 3/3] #496: Add tests for UriBuilder. Fix bug in UriBuilder where query params are not parsed correctly. --- src/main/java/org/stellar/sdk/UriBuilder.java | 12 ++- .../java/org/stellar/sdk/UriBuilderTest.java | 96 +++++++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/stellar/sdk/UriBuilderTest.java diff --git a/src/main/java/org/stellar/sdk/UriBuilder.java b/src/main/java/org/stellar/sdk/UriBuilder.java index f6197a4fb..23c4a9e39 100644 --- a/src/main/java/org/stellar/sdk/UriBuilder.java +++ b/src/main/java/org/stellar/sdk/UriBuilder.java @@ -136,14 +136,20 @@ private static LinkedHashMap parseQueryParams(String queryParams return new LinkedHashMap<>(); } else { final var paramsMap = new LinkedHashMap(); - final var pattern = Pattern.compile("\\??(?:&?[^=&]*=[^=&]*)*"); + final var pattern = Pattern.compile("\\??(&?[^=&]*=[^=&]*)"); final var matcher = pattern.matcher(queryParamsString); while (matcher.find()) { final var parts = matcher.group(1).split("=", 2); + var name = parts[0]; + + if (name.startsWith("&")) { + name = name.substring(1); + } + if (parts.length == 1 || StringUtil.isBlank(parts[1])) { - paramsMap.put(parts[0], ""); + paramsMap.put(name, ""); } else { - paramsMap.put(parts[0], parts[1]); + paramsMap.put(name, parts[1]); } } diff --git a/src/test/java/org/stellar/sdk/UriBuilderTest.java b/src/test/java/org/stellar/sdk/UriBuilderTest.java new file mode 100644 index 000000000..62fbaaa7a --- /dev/null +++ b/src/test/java/org/stellar/sdk/UriBuilderTest.java @@ -0,0 +1,96 @@ +package org.stellar.sdk; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import java.net.URI; +import org.junit.Test; + +public class UriBuilderTest { + @Test + public void testFromUriString() throws Exception { + final var builder = new UriBuilder("https://google.com"); + assertEquals(new URI("https://google.com"), builder.build()); + } + + @Test + public void testFromUriStringHandlesQueryParams() throws Exception { + final var builder = new UriBuilder("https://google.com?param1=value1¶m2=value2"); + assertEquals("value1", builder.getQueryParameter("param1").get()); + assertEquals("value2", builder.getQueryParameter("param2").get()); + } + + @Test + public void testFromUri() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com")); + assertEquals(new URI("https://google.com"), builder.build()); + } + + @Test + public void testSetSchemeThrowsBlank() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com")); + assertThrows(IllegalArgumentException.class, () -> builder.setScheme("")); + } + + @Test + public void testSetScheme() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com")); + builder.setScheme("ftp"); + assertEquals("ftp", builder.build().getScheme()); + } + + @Test + public void testSetHostThrowsBlank() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com")); + assertThrows(IllegalArgumentException.class, () -> builder.setHost("")); + } + + @Test + public void testSetHost() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com")); + builder.setHost("goooooogle"); + assertEquals("goooooogle", builder.build().getHost()); + } + + @Test + public void testSetPortAllowsNull() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com:8080")); + builder.setPort(null); + assertEquals(new URI("https://google.com"), builder.build()); + } + + @Test + public void testSetPortInvalid() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com:8080")); + assertThrows(IllegalArgumentException.class, () -> builder.setPort(-1)); + assertThrows(IllegalArgumentException.class, () -> builder.setPort(65536)); + } + + @Test + public void testSetQueryParam() throws Exception { + final var builder = new UriBuilder("https://google.com"); + builder.setQueryParameter("param", "value"); + assertEquals(new URI("https://google.com?param=value"), builder.build()); + } + + @Test + public void testSetMultipleQueryParams() throws Exception { + final var builder = new UriBuilder("https://google.com"); + builder.setQueryParameter("param1", "value1"); + builder.setQueryParameter("param2", "value2"); + assertEquals(new URI("https://google.com?param1=value1¶m2=value2"), builder.build()); + } + + @Test + public void testAddPathSegmentBlankThrows() throws Exception { + final var builder = new UriBuilder(new URI("https://google.com:8080")); + assertThrows(IllegalArgumentException.class, () -> builder.addPathSegment("")); + } + + @Test + public void testAddPathSegment() throws Exception { + final var builder = new UriBuilder("https://google.com"); + builder.addPathSegment("path"); + assertEquals(new URI("https://google.com/path"), builder.build()); + } +}