Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ secring.gpg
## Plugin-specific files:

# IntelliJ
/out/
out/

# mpeltonen/sbt-idea plugin
.idea_modules/
Expand Down Expand Up @@ -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
9 changes: 8 additions & 1 deletion lombok.config
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
config.stopBubbling = true
lombok.addLombokGeneratedAnnotation = true
lombok.nonnull.exceptiontype=IllegalArgumentException
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
1 change: 1 addition & 0 deletions okhttp-spring-boot-autoconfigure/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -59,7 +67,8 @@ public OkHttpClient okHttp3Client(
ObjectProvider<HostnameVerifier> hostnameVerifier,
ObjectProvider<CertificatePinner> certificatePinner,
ConnectionPool connectionPool,
ObjectProvider<EventListener> eventListener
ObjectProvider<EventListener> eventListener,
ObjectProvider<Dispatcher> dispatcher
) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();

Expand Down Expand Up @@ -95,6 +104,8 @@ public OkHttpClient okHttp3Client(

configurers.forEach(configurer -> configurer.configure(builder));

dispatcher.ifUnique(builder::dispatcher);

return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.io.File;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
* @author Lars Grefer
Expand All @@ -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);

Expand Down Expand Up @@ -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<Protocol> protocols = null;

Expand Down
2 changes: 2 additions & 0 deletions okhttp-spring-client/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<? extends String, ? extends String> okhttpHeader : okhttpHeaders) {
springHeaders.add(okhttpHeader.getFirst(), okhttpHeader.getSecond());
Expand All @@ -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<? extends @Nullable Object> 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<? extends @Nullable Object> 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" <code>/</code> at the end.
@param addTrailingSlash false without final <code>/</code>; true always with final <code>/</code> 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<String> 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<String> 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;
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand Down
Loading