diff --git a/.editorconfig b/.editorconfig index eea8f787..91ecd9d7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,7 @@ charset = utf-8 insert_final_newline = true indent_style = space trim_trailing_whitespace = true +tab_width = 4 +indent_size = 4 +ij_continuation_indent_size = 8 +end_of_line = lf diff --git a/.gitignore b/.gitignore index dcb35b02..fabac956 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,7 @@ secring.gpg ## Plugin-specific files: # IntelliJ -/out/ +out/ # mpeltonen/sbt-idea plugin .idea_modules/ @@ -73,4 +73,4 @@ gradle-app.setting # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 # gradle/wrapper/gradle-wrapper.properties -# End of https://www.gitignore.io/api/intellij,gradle +# End of https://www.gitignore.io/api/intellij,gradle \ No newline at end of file diff --git a/lombok.config b/lombok.config index 1eadded3..059aba9b 100644 --- a/lombok.config +++ b/lombok.config @@ -1,3 +1,10 @@ config.stopBubbling = true lombok.addLombokGeneratedAnnotation = true -lombok.nonnull.exceptiontype=IllegalArgumentException \ No newline at end of file +lombok.nonnull.exceptiontype=IllegalArgumentException + +lombok.addSuppressWarnings = true + +# https://docs.spring.io/spring-framework/reference/7.0/core/null-safety.html ➕ http://jspecify.org/docs/user-guide/ ➕ https://github.com/uber/NullAway/issues/917 +lombok.addNullAnnotations = jspecify + +lombok.var.flagUsage = error \ No newline at end of file diff --git a/okhttp-spring-boot-autoconfigure/build.gradle b/okhttp-spring-boot-autoconfigure/build.gradle index 02e3386d..11426d4e 100644 --- a/okhttp-spring-boot-autoconfigure/build.gradle +++ b/okhttp-spring-boot-autoconfigure/build.gradle @@ -28,6 +28,7 @@ dependencies { api 'org.springframework.boot:spring-boot-autoconfigure' implementation 'org.slf4j:slf4j-api' + api 'org.jspecify:jspecify' optional project(':okhttp-spring-client') optional 'org.springframework:spring-web' diff --git a/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttp3AutoConfiguration.java b/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttp3AutoConfiguration.java index 208040b8..7da80335 100644 --- a/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttp3AutoConfiguration.java +++ b/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttp3AutoConfiguration.java @@ -5,7 +5,15 @@ import io.freefair.spring.okhttp.OkHttp3Configurer; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; -import okhttp3.*; +import okhttp3.Cache; +import okhttp3.CertificatePinner; +import okhttp3.ConnectionPool; +import okhttp3.CookieJar; +import okhttp3.Dispatcher; +import okhttp3.Dns; +import okhttp3.EventListener; +import okhttp3.Interceptor; +import okhttp3.OkHttpClient; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -59,7 +67,8 @@ public OkHttpClient okHttp3Client( ObjectProvider hostnameVerifier, ObjectProvider certificatePinner, ConnectionPool connectionPool, - ObjectProvider eventListener + ObjectProvider eventListener, + ObjectProvider dispatcher ) { OkHttpClient.Builder builder = new OkHttpClient.Builder(); @@ -95,6 +104,8 @@ public OkHttpClient okHttp3Client( configurers.forEach(configurer -> configurer.configure(builder)); + dispatcher.ifUnique(builder::dispatcher); + return builder.build(); } diff --git a/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttpProperties.java b/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttpProperties.java index 85d84f51..74c773b9 100644 --- a/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttpProperties.java +++ b/okhttp-spring-boot-autoconfigure/src/main/java/io/freefair/spring/okhttp/autoconfigure/OkHttpProperties.java @@ -9,6 +9,7 @@ import java.io.File; import java.time.Duration; import java.util.List; +import java.util.concurrent.TimeUnit; /** * @author Lars Grefer @@ -19,6 +20,8 @@ public class OkHttpProperties { /** * The default connect timeout for new connections. + * @see okhttp3.OkHttpClient.Builder#connectTimeout(java.time.Duration) + * @see okhttp3.Interceptor.Chain#withConnectTimeout(int, TimeUnit) */ private Duration connectTimeout = Duration.ofSeconds(10); @@ -61,7 +64,7 @@ public class OkHttpProperties { private boolean retryOnConnectionFailure = true; /** - * Configure the protocols used by this client to communicate with remote servers. + * Configure the {@link Protocol Protocols} used by this client to communicate with remote servers. */ private List protocols = null; diff --git a/okhttp-spring-client/build.gradle b/okhttp-spring-client/build.gradle index 6ebca82e..9c08b76e 100644 --- a/okhttp-spring-client/build.gradle +++ b/okhttp-spring-client/build.gradle @@ -11,9 +11,11 @@ dependencies { api "com.squareup.okhttp3:okhttp" api "org.springframework:spring-web" + api 'org.jspecify:jspecify' testImplementation "org.springframework.boot:spring-boot-starter-test" testImplementation "org.springframework.boot:spring-boot-starter-web" + testImplementation("org.instancio:instancio-junit:latest.release") } tasks.named("test", Test) { diff --git a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/OkHttpUtils.java b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/OkHttpUtils.java index 0d6830e1..00e3d335 100644 --- a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/OkHttpUtils.java +++ b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/OkHttpUtils.java @@ -1,17 +1,29 @@ package io.freefair.spring.okhttp; import kotlin.Pair; +import lombok.NonNull; import lombok.experimental.UtilityClass; +import lombok.val; import okhttp3.Headers; +import okhttp3.HttpUrl; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; -import org.springframework.util.Assert; +import java.util.Collection; +import java.util.List; + +/** + @see okhttp3.Headers.Builder + @see org.springframework.http.HttpHeaders + */ @UtilityClass +@NullMarked public class OkHttpUtils { + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - public static HttpHeaders toSpringHeaders(Headers okhttpHeaders) { - Assert.notNull(okhttpHeaders, "Headers must not be null"); - HttpHeaders springHeaders = new HttpHeaders(); + public static HttpHeaders toSpringHeaders (@NonNull Headers okhttpHeaders) { + var springHeaders = new HttpHeaders(); for (Pair okhttpHeader : okhttpHeaders) { springHeaders.add(okhttpHeader.getFirst(), okhttpHeader.getSecond()); @@ -20,13 +32,127 @@ public static HttpHeaders toSpringHeaders(Headers okhttpHeaders) { return springHeaders; } - public static Headers toOkHttpHeaders(HttpHeaders springHeaders) { - Headers.Builder builder = new Headers.Builder(); + public static Headers toOkHttpHeaders (HttpHeaders springHeaders) { + var builder = new Headers.Builder(); - springHeaders.forEach((name, values) -> { - values.forEach(value -> builder.add(name, value)); - }); + springHeaders.forEach((name, values) -> + values.forEach(value -> builder.add(name, value)) + ); return builder.build(); } + + public static boolean nonEmpty (@Nullable CharSequence str) { + return str != null && str.length() > 0; + } + + public static boolean nonEmpty (@Nullable Collection items) { + return items != null && !items.isEmpty(); + } + + public static boolean found (int indexOf) { + return indexOf >= 0; + } + + public static int len (@Nullable CharSequence str) { + return str != null ? str.length() : 0; + } + + public static int len (@Nullable Collection items) { + return items != null ? items.size() : 0; + } + + /** + Get {@link HttpUrl.Builder} for the pre-built url bypassing {@link HttpUrl#newBuilder()} or {@link HttpUrl#newBuilder(String)} i.e: + without creating {@link HttpUrl} beforehand. + + That is, there is an url-template, and it is necessary to slightly modify it, e.g.: substitute variables. + */ + public static HttpUrl.Builder urlBuilder (String startingUrl) { + return new HttpUrl.Builder().parse$okhttp(null/*@Nullable HttpUrl base*/, startingUrl); + } + + /** + Copy of {@link HttpUrl.Builder#toString()}, but without the "extra" / at the end. + @param addTrailingSlash false without final /; true always with final / including /with/path/ + @see HttpUrl#toString() + */ + public static String toString (HttpUrl.Builder b, boolean addTrailingSlash) { + val sb = new StringBuilder(127); + String scheme = b.getScheme$okhttp(); + if (scheme != null){ + sb.append(scheme).append("://"); + } else { + sb.append("//"); + } + + String encodedPassword = b.getEncodedPassword$okhttp(); + if (nonEmpty(b.getEncodedUsername$okhttp()) || nonEmpty(encodedPassword)){ + sb.append(b.getEncodedUsername$okhttp()); + if (nonEmpty(encodedPassword)){ + sb.append(':').append(encodedPassword); + } + sb.append('@'); + } + + String host = b.getHost$okhttp(); + if (host != null){ + if (found(host.indexOf(':'))){// Host is an IPv6 address. + sb.append('[').append(host).append(']'); + } else { + sb.append(host); + } + } + + if (b.getPort$okhttp() >= 0 || scheme != null){ + val effectivePort = effectivePort(b.getPort$okhttp(), scheme); + if (effectivePort != defaultPort(scheme)){ + sb.append(':').append(effectivePort); + } + } + + // encodedPathSegments.toPathString(this) + List encodedPathSegments = b.getEncodedPathSegments$okhttp(); + for (var ps : encodedPathSegments){ + if (nonEmpty(ps)){// let's get rid of both the final / and // (empty pathSegment: has the right to live, but Spring cleans up) + sb.append('/').append(ps); + } + } + if (addTrailingSlash){ + sb.append('/'); + } + + // encodedQueryNamesAndValues!!.toQueryString(this) + List query = b.getEncodedQueryNamesAndValues$okhttp(); + if (nonEmpty(query)){ + sb.append('?'); + for (var it = query.iterator(); it.hasNext();){ + sb.append(it.next());// key + String value = it.hasNext() ? it.next() : null; + if (value != null){ + sb.append('=').append(value); + } + if (it.hasNext()){ + sb.append('&');// there's someone else behind us + } + } + } + + if (nonEmpty(b.getEncodedFragment$okhttp())){ + sb.append('#').append(b.getEncodedFragment$okhttp()); + } + return sb.toString(); + } + private static int effectivePort (int port, @Nullable String scheme) { + return port >= 0 || scheme == null ? port + : defaultPort(scheme); + } + private static int defaultPort (String scheme) { + return switch(scheme){ + case "http","HTTP" -> 80; + case "https","HTTPS" -> 443; + default -> -1; + }; + } + } diff --git a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/ResourceRequestBody.java b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/ResourceRequestBody.java index f89f24ea..4bebdae8 100644 --- a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/ResourceRequestBody.java +++ b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/ResourceRequestBody.java @@ -4,25 +4,28 @@ import okhttp3.RequestBody; import okio.BufferedSink; import okio.Okio; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import java.io.IOException; import java.io.InputStream; /** + * @see org.springframework.core.io.Resource + * @see org.springframework.util.MimeType + * @see okhttp3.RequestBody + * @see okhttp3.MediaType * @author Lars Grefer */ public class ResourceRequestBody extends RequestBody { private final Resource resource; - @Nullable - private final MediaType mediaType; + private final @Nullable MediaType mediaType; public ResourceRequestBody(Resource resource) { this.resource = resource; @@ -34,14 +37,13 @@ public ResourceRequestBody(Resource resource, MimeType springMimeType) { this.mediaType = MediaType.parse(springMimeType.toString()); } - public ResourceRequestBody(Resource resource, MediaType okhttpMediaType) { + public ResourceRequestBody(Resource resource, @Nullable MediaType okhttpMediaType) { this.resource = resource; this.mediaType = okhttpMediaType; } @Override - @Nullable - public MediaType contentType() { + public @Nullable MediaType contentType() { return mediaType; } diff --git a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java index b92903d4..81137d75 100644 --- a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java +++ b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequest.java @@ -1,7 +1,13 @@ package io.freefair.spring.okhttp.client; +import lombok.Getter; +import lombok.NonNull; import lombok.RequiredArgsConstructor; -import okhttp3.*; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; import okio.Buffer; import okio.ByteString; import org.springframework.http.HttpHeaders; @@ -19,20 +25,25 @@ import java.net.MalformedURLException; import java.net.URI; +import static io.freefair.spring.okhttp.OkHttpUtils.EMPTY_BYTE_ARRAY; + /** * OkHttp based {@link ClientHttpRequest} implementation. * * @author Lars Grefer * @see OkHttpClientRequestFactory + * @see org.springframework.http.client.OkHttp3ClientHttpRequest + * @see org.springframework.http.client.AbstractStreamingClientHttpRequest */ @RequiredArgsConstructor public class OkHttpClientRequest extends AbstractClientHttpRequest implements StreamingHttpOutputMessage { - private final OkHttpClient okHttpClient; + @Getter private final OkHttpClient okHttpClient; private final URI uri; - private final HttpMethod method; + /*** @see org.springframework.http.HttpRequest#getMethod */ + @Getter(onMethod_=@Override) private final HttpMethod method; @Nullable @@ -42,19 +53,13 @@ public class OkHttpClientRequest extends AbstractClientHttpRequest implements St private Buffer bufferBody; - @Override - public HttpMethod getMethod() { - return method; - } - @Override public URI getURI() { return uri; } @Override - public void setBody(Body body) { - Assert.notNull(body, "body must not be null"); + public void setBody (@NonNull Body body) { assertNotExecuted(); Assert.state(bufferBody == null, "getBody has already been used."); this.streamingBody = body; @@ -105,7 +110,7 @@ private Request buildRequest(HttpHeaders headers) throws MalformedURLException { } else if (streamingBody != null) { body = new StreamingBodyRequestBody(streamingBody, contentType, headers.getContentLength()); } else if (okhttp3.internal.http.HttpMethod.requiresRequestBody(method.name())) { - body = RequestBody.create(new byte[0], contentType); + body = RequestBody.create(EMPTY_BYTE_ARRAY, contentType); } builder.method(getMethod().name(), body); diff --git a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java index 717f12c7..725be666 100644 --- a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java +++ b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientRequestFactory.java @@ -1,10 +1,10 @@ package io.freefair.spring.okhttp.client; +import lombok.NonNull; import okhttp3.OkHttpClient; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.lang.NonNull; import java.net.URI; diff --git a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java index 36b4ae67..c2bf4560 100644 --- a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java +++ b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/OkHttpClientResponse.java @@ -1,6 +1,7 @@ package io.freefair.spring.okhttp.client; import io.freefair.spring.okhttp.OkHttpUtils; +import lombok.Getter; import lombok.RequiredArgsConstructor; import okhttp3.Response; import okhttp3.ResponseBody; @@ -15,11 +16,12 @@ * * @author Lars Grefer * @see OkHttpClientRequest + * @see org.springframework.http.client.OkHttp3ClientHttpResponse */ @RequiredArgsConstructor public class OkHttpClientResponse implements ClientHttpResponse { - private final Response okHttpResponse; + @Getter private final Response okHttpResponse; private HttpHeaders springHeaders; diff --git a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java index 540999d0..2f2de01d 100644 --- a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java +++ b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/StreamingBodyRequestBody.java @@ -1,12 +1,14 @@ package io.freefair.spring.okhttp.client; +import lombok.Getter; +import lombok.NonNull; import lombok.RequiredArgsConstructor; +import lombok.experimental.Accessors; import okhttp3.MediaType; import okhttp3.RequestBody; import okio.BufferedSink; -import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.Nullable; import org.springframework.http.StreamingHttpOutputMessage; -import org.springframework.lang.Nullable; import java.io.IOException; @@ -23,22 +25,15 @@ class StreamingBodyRequestBody extends RequestBody { private final MediaType contentType; - @Nullable - private final long contentLength; + @Getter(onMethod_=@Override) @Accessors(fluent = true) private final long contentLength; - @Nullable @Override - public MediaType contentType() { + public @Nullable MediaType contentType() { return contentType; } @Override - public long contentLength() { - return contentLength; - } - - @Override - public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException { + public void writeTo(@NonNull BufferedSink bufferedSink) throws IOException { streamingBody.writeTo(bufferedSink.outputStream()); } diff --git a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/package-info.java b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/package-info.java index adba992e..0db4cb24 100644 --- a/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/package-info.java +++ b/okhttp-spring-client/src/main/java/io/freefair/spring/okhttp/client/package-info.java @@ -1,4 +1,4 @@ -@NonNullApi +@NullMarked package io.freefair.spring.okhttp.client; -import org.springframework.lang.NonNullApi; +import org.jspecify.annotations.NullMarked; diff --git a/okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/OkHttpUtilsTest.java b/okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/OkHttpUtilsTest.java index 1f667aaf..3f199709 100644 --- a/okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/OkHttpUtilsTest.java +++ b/okhttp-spring-client/src/test/java/io/freefair/spring/okhttp/OkHttpUtilsTest.java @@ -1,7 +1,10 @@ package io.freefair.spring.okhttp; +import lombok.val; import okhttp3.Headers; +import okhttp3.HttpUrl; import org.assertj.core.api.Assertions; +import org.instancio.Instancio; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -10,14 +13,16 @@ import java.time.temporal.ChronoUnit; import java.util.List; +import static io.freefair.spring.okhttp.OkHttpUtils.len; +import static io.freefair.spring.okhttp.OkHttpUtils.urlBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +/*** @see OkHttpUtils */ class OkHttpUtilsTest { - @Test void headerConversion() { - HttpHeaders springHeaders = new HttpHeaders(); Instant date = Instant.now(); @@ -32,7 +37,164 @@ void headerConversion() { assertThat(okHttpHeaders.get("Accept-Charset")).isEqualToIgnoringCase("UTF-8"); assertThat(OkHttpUtils.toSpringHeaders(okHttpHeaders)).isEqualTo(springHeaders); + } + + @Test + void _builder () { + HttpUrl.Builder b = urlBuilder("http:ya.ru/papa/:xyz/?q=x"); + b.scheme("https"); + b.host("yandex.com"); + b.port(8080); + b.addPathSegments("mama"); + List paths = b.getEncodedPathSegments$okhttp(); + for (int i = 0; i < paths.size(); i++){ + String pathElement = paths.get(i); + if (":xyz".equalsIgnoreCase(pathElement)){ + b.setPathSegment(i, "42");// in reality, there is a search in the Map with variables + } + } + b.addQueryParameter("foo", "bar"); + b.addQueryParameter("foo", "Zoo"); + String s = "https://yandex.com:8080/papa/42/mama?q=x&foo=bar&foo=Zoo"; + assertEquals(s, b.toString()); + assertEquals(s, b.build().toString()); + assertEquals(s, b.build().url().toString()); + assertEquals(s, OkHttpUtils.toString(b, false)); + } + + @Test + void _toStringDemo () { + var b = urlBuilder("http:/raw.net"); + String s = "http://raw.net/"; + assertEquals(s, b.toString()); + assertEquals(s, b.build().toString()); + assertEquals(s, OkHttpUtils.toString(b, true)); + assertEquals("http://raw.net", OkHttpUtils.toString(b, false)); + + b = urlBuilder("http:/raw.net/"); + s = "http://raw.net/"; + assertEquals(s, b.toString()); + assertEquals(s, b.build().toString()); + assertEquals(s, OkHttpUtils.toString(b, true)); + assertEquals("http://raw.net", OkHttpUtils.toString(b, false)); + + b = urlBuilder("https://with.port.org:42"); + s = "https://with.port.org:42/"; + assertEquals(s, b.toString()); + assertEquals(s, b.build().toString()); + assertEquals(s, OkHttpUtils.toString(b, true)); + assertEquals("https://with.port.org:42", OkHttpUtils.toString(b, false)); + + b = urlBuilder("HTTPS:with.port.org:443"); + s = "https://with.port.org/"; + assertEquals(s, b.toString()); + assertEquals(s, b.build().toString()); + assertEquals(s, OkHttpUtils.toString(b, true)); + assertEquals("https://with.port.org", OkHttpUtils.toString(b, false)); + + + s = "https://example.com/foo/bar?x=y"; + b = urlBuilder(s); + assertEquals(s, b.toString()); + assertEquals(s, b.build().toString()); + assertEquals("https://example.com/foo/bar/?x=y", OkHttpUtils.toString(b, true)); + assertEquals(s, OkHttpUtils.toString(b, false)); + + s = "https://example.com/foo/bar/?x=y#boo"; + b = urlBuilder(s); + assertEquals(s, b.toString()); + assertEquals(s, b.build().toString()); + assertEquals(s, OkHttpUtils.toString(b, true)); + assertEquals("https://example.com/foo/bar?x=y#boo", OkHttpUtils.toString(b, false)); + } + + @Test + void _ourBuilderToString () { + HttpUrl.Builder b = urlBuilder("http:ya.ru"); + assertEquals("http://ya.ru/", b.toString()); + assertEquals("http://ya.ru", OkHttpUtils.toString(b, false)); + + b.addPathSegments("foo"); + assertEquals("http://ya.ru/foo", b.toString()); + assertEquals("http://ya.ru/foo", OkHttpUtils.toString(b, false)); + + b.addQueryParameter("q","text"); + assertEquals("http://ya.ru/foo?q=text", OkHttpUtils.toString(b, false)); + + b.port(9042); + assertEquals("http://ya.ru:9042/foo?q=text", OkHttpUtils.toString(b, false)); + + b.removePathSegment(0); + assertEquals("http://ya.ru:9042?q=text", OkHttpUtils.toString(b, false)); + + for (int loop = 0; loop < 10_000; loop++){ + val builder = Instancio.create(HttpUrl.Builder.class); + if ((len(builder.getEncodedQueryNamesAndValues$okhttp()) & 1) == 1){ + builder.getEncodedQueryNamesAndValues$okhttp().add(null);// value must exists + } + String url = builder.toString(); + if (url.endsWith("/")){ + url = url.substring(0, url.length()-1);// cut out last / + } + url = url.replace("/?", "?"); + assertEquals(url, OkHttpUtils.toString(builder, false)); + } + } + + @Test + void _ourBuilderToStringANTI () { + HttpUrl.Builder b = urlBuilder("http:ya.ru"); + assertEquals("http://ya.ru/", b.toString()); + assertEquals("http://ya.ru/", OkHttpUtils.toString(b, true)); + + b.addPathSegments("foo"); + assertEquals("http://ya.ru/foo", b.toString()); + assertEquals("http://ya.ru/foo/", OkHttpUtils.toString(b, true)); + + b.addQueryParameter("q","text"); + assertEquals("http://ya.ru/foo/?q=text", OkHttpUtils.toString(b, true)); + + b.port(9042); + assertEquals("http://ya.ru:9042/foo/?q=text", OkHttpUtils.toString(b, true)); + + b.removePathSegment(0); + assertEquals("http://ya.ru:9042/?q=text", OkHttpUtils.toString(b, true)); + + b.addQueryParameter("zzz", null); + assertEquals("http://ya.ru:9042/?q=text&zzz", OkHttpUtils.toString(b, true)); + } + + @Test + void _ourBuilderToStringTwoSlashes () { + HttpUrl.Builder b = urlBuilder("http:ya.ru//"); + assertEquals("http://ya.ru//", b.toString()); + assertEquals("http://ya.ru", OkHttpUtils.toString(b, false)); + assertEquals("http://ya.ru/", OkHttpUtils.toString(b, true)); + b.addPathSegments(""); + assertEquals("http://ya.ru//", b.toString()); + assertEquals("http://ya.ru", OkHttpUtils.toString(b, false)); + assertEquals("http://ya.ru/", OkHttpUtils.toString(b, true)); + b.addPathSegments(""); + assertEquals("http://ya.ru//", b.toString()); + assertEquals("http://ya.ru", OkHttpUtils.toString(b, false)); + assertEquals("http://ya.ru/", OkHttpUtils.toString(b, true)); + b.addEncodedPathSegment(""); + assertEquals("http://ya.ru//", b.toString()); + assertEquals("http://ya.ru", OkHttpUtils.toString(b, false)); + assertEquals("http://ya.ru/", OkHttpUtils.toString(b, true)); + b.addEncodedPathSegments(""); + assertEquals("http://ya.ru//", b.toString()); + assertEquals("http://ya.ru", OkHttpUtils.toString(b, false)); + assertEquals("http://ya.ru/", OkHttpUtils.toString(b, true)); + b.port(80);// default http port ⇒ ignore + assertEquals("http://ya.ru//", b.toString()); + assertEquals("http://ya.ru", OkHttpUtils.toString(b, false)); + assertEquals("http://ya.ru/", OkHttpUtils.toString(b, true)); + b.port(8090); + assertEquals("http://ya.ru:8090//", b.toString()); + assertEquals("http://ya.ru:8090", OkHttpUtils.toString(b, false)); + assertEquals("http://ya.ru:8090/", OkHttpUtils.toString(b, true)); } }