OkHttpWpRestClient.java
package io.github.evisentin.wordpress.rest.client.adapter.okhttp;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.evisentin.wordpress.rest.client.adapter.okhttp.discovery.ApiUrlDiscoveryHelper;
import io.github.evisentin.wordpress.rest.client.adapter.okhttp.interceptors.AuthenticationInterceptor;
import io.github.evisentin.wordpress.rest.client.adapter.okhttp.interceptors.WpErrorInterceptor;
import io.github.evisentin.wordpress.rest.client.adapter.okhttp.modules.*;
import io.github.evisentin.wordpress.rest.client.domain.WpBaseRestClient;
import io.github.evisentin.wordpress.rest.client.domain.api.*;
import io.github.evisentin.wordpress.rest.client.domain.auth.WpAuthenticationStrategy;
import io.github.evisentin.wordpress.rest.client.domain.configuration.SslConfiguration;
import io.github.evisentin.wordpress.rest.client.domain.configuration.TimeoutConfiguration;
import lombok.NonNull;
import lombok.SneakyThrows;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import java.util.Arrays;
import static org.apache.commons.lang3.ObjectUtils.anyNull;
/**
* OkHttp-based implementation of {@link WpBaseRestClient}.
*
* <p>
* This client provides a synchronous wrapper around the WordPress REST API using {@link okhttp3.OkHttpClient} as the
* underlying HTTP transport.
*
* <p>
* It supports CRUD operations and paginated queries for core WordPress resources such as posts, categories, and tags.
*
* <p>
* The client is configured with an authentication strategy and automatically registers internal interceptors for:
* <ul>
* <li>Authentication ({@link AuthenticationInterceptor})</li>
* <li>WordPress-specific error handling ({@link WpErrorInterceptor})</li>
* </ul>
*
* <p>
* Optional {@link SslConfiguration} and {@link TimeoutConfiguration} can be provided
* to customize TLS behaviour and request timeouts.
*
* <b>Thread Safety</b>
*
* <p>
* Instances of this class are thread-safe and intended to be reused.
*
* @see WpBaseRestClient
* @see AuthenticationInterceptor
* @see WpErrorInterceptor
*/
public class OkHttpWpRestClient extends WpBaseRestClient {
private final CategoryAPIs categoryAPIs;
private final CommentAPIs commentAPIs;
private final MediaAPIs mediaAPIs;
private final PageAPIs pageAPIs;
private final PageRevisionAPIs pageRevisionAPIs;
private final PostAPIs postAPIs;
private final PostRevisionAPIs postRevisionAPIs;
private final PostStatusAPIs postStatusAPIs;
private final PostTypeAPIs postTypeAPIs;
private final TagAPIs tagAPIs;
private final TaxonomyAPIs taxonomyAPIs;
/**
* Creates a new {@code OkHttpWpRestClient}.
*
* <p>
* The client is initialized with the given base URL and authentication strategy, and backed by an
* {@link okhttp3.OkHttpClient} configured with internal authentication and error-handling interceptors.
*
* <p>
* If a {@link SslConfiguration} is provided, it is applied to the underlying HTTP client. If {@code null}, the
* default JVM SSL configuration is used.
*
* <p>
* If a {@link TimeoutConfiguration} is provided, it is applied to the HTTP client. If {@code null}, OkHttp defaults
* are used.
*
* @param baseUrl
* the base URL of the WordPress instance (must not be {@code null})
* @param authenticationStrategy
* the authentication strategy used to sign requests (must not be {@code null})
* @param sslConfiguration
* optional SSL configuration; may be {@code null}
* @param timeoutConfiguration
* optional timeout configuration; may be {@code null}
* @param interceptors
* additional OkHttp interceptors to register with the underlying HTTP client; may be empty
*
* @throws NullPointerException
* if {@code baseUrl} or {@code authenticationStrategy} is {@code null}
* @throws IllegalStateException
* if the provided {@link SslConfiguration} is invalid
*/
@SneakyThrows
OkHttpWpRestClient(final @NonNull String baseUrl,
final @NonNull WpAuthenticationStrategy authenticationStrategy,
final SslConfiguration sslConfiguration,
final TimeoutConfiguration timeoutConfiguration,
final Interceptor... interceptors) {
ObjectMapper mapper = new ObjectMapper();
mapper.findAndRegisterModules();
final OkHttpClient authHttpClient = buildAuthHttpClient(sslConfiguration, timeoutConfiguration);
String apiUrl = ApiUrlDiscoveryHelper.resolveApiUrl(authHttpClient, baseUrl);
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder()
.addInterceptor(new AuthenticationInterceptor(authenticationStrategy, authHttpClient, apiUrl))
.addInterceptor(new WpErrorInterceptor());
Arrays.stream(interceptors).forEach(clientBuilder::addInterceptor);
applySslConfigurationIfPresent(clientBuilder, sslConfiguration);
applyTimeoutConfigurationIfPresent(clientBuilder, timeoutConfiguration);
OkHttpClient httpClient = clientBuilder.build();
categoryAPIs = new CategoryApiClientModule(apiUrl, httpClient, mapper);
commentAPIs = new CommentApiClientModule(apiUrl, httpClient, mapper);
mediaAPIs = new MediaApiClientModule(apiUrl, httpClient, mapper);
pageAPIs = new PageApiClientModule(apiUrl, httpClient, mapper);
pageRevisionAPIs = new PageRevisionApiClientModule(apiUrl, httpClient, mapper);
postAPIs = new PostApiClientModule(apiUrl, httpClient, mapper);
postRevisionAPIs = new PostRevisionApiClientModule(apiUrl, httpClient, mapper);
postStatusAPIs = new PostStatusApiClientModule(apiUrl, httpClient, mapper);
postTypeAPIs = new PostTypeApiClientModule(apiUrl, httpClient, mapper);
tagAPIs = new TagApiClientModule(apiUrl, httpClient, mapper);
taxonomyAPIs = new TaxonomyApiClientModule(apiUrl, httpClient, mapper);
}
@Override
public CategoryAPIs categories() {
return categoryAPIs;
}
@Override
public CommentAPIs comments() {
return commentAPIs;
}
@Override
public MediaAPIs media() {
return mediaAPIs;
}
@Override
public PageRevisionAPIs pageRevisions() {
return pageRevisionAPIs;
}
@Override
public PageAPIs pages() {
return pageAPIs;
}
@Override
public PostRevisionAPIs postRevisions() {
return postRevisionAPIs;
}
@Override
public PostStatusAPIs postStatuses() {
return postStatusAPIs;
}
@Override
public PostTypeAPIs postTypes() {
return postTypeAPIs;
}
@Override
public PostAPIs posts() {
return postAPIs;
}
@Override
public TagAPIs tags() {
return tagAPIs;
}
@Override
public TaxonomyAPIs taxonomies() {
return taxonomyAPIs;
}
private void applyTimeoutConfigurationIfPresent(final OkHttpClient.Builder clientBuilder,
final TimeoutConfiguration config) {
if (config != null) {
clientBuilder.connectTimeout(config.getConnectTimeout())
.readTimeout(config.getReadTimeout())
.writeTimeout(config.getWriteTimeout())
.callTimeout(config.getCallTimeout());
}
}
private OkHttpClient buildAuthHttpClient(final SslConfiguration sslConfiguration,
final TimeoutConfiguration timeoutConfiguration) {
OkHttpClient.Builder authClientBuilder = new OkHttpClient.Builder();
applySslConfigurationIfPresent(authClientBuilder, sslConfiguration);
applyTimeoutConfigurationIfPresent(authClientBuilder, timeoutConfiguration);
return authClientBuilder.build();
}
private static void applySslConfigurationIfPresent(final OkHttpClient.Builder clientBuilder,
final SslConfiguration sslConfiguration) {
if (sslConfiguration != null) {
failOnInvalidConfiguration(sslConfiguration);
clientBuilder.sslSocketFactory(sslConfiguration.getSslSocketFactory(), sslConfiguration.getTrustManager());
if (sslConfiguration.getHostnameVerifier() != null) {
clientBuilder.hostnameVerifier(sslConfiguration.getHostnameVerifier());
}
}
}
private static void failOnInvalidConfiguration(final SslConfiguration sslConfiguration) {
if (anyNull(sslConfiguration.getSslSocketFactory(), sslConfiguration.getTrustManager())) {
throw new IllegalStateException("SSL configuration requires both sslSocketFactory and trustManager");
}
}
}