ApiClientModule.java

package io.github.evisentin.wordpress.rest.client.adapter.apache.modules;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.evisentin.wordpress.rest.client.domain.model.WpPagedResponse;
import io.github.evisentin.wordpress.rest.client.domain.model.query.WpPaginationQuery;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.text.StringSubstitutor;
import org.apache.hc.client5.http.classic.methods.HttpDelete;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.net.URIBuilder;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static java.util.Optional.ofNullable;
import static org.apache.hc.core5.http.HttpHeaders.ACCEPT;

/**
 * Base class for Apache HttpClient-backed WordPress API modules.
 *
 * <p>This class provides common functionality for executing HTTP requests, handling paginated responses, performing
 * JSON serialization and deserialization, uploading files, and building endpoint URIs.</p>
 *
 * <p>Concrete module implementations extend this class to interact with specific WordPress REST API resources.</p>
 */
@RequiredArgsConstructor
abstract class ApiClientModule {
    /**
     * Template variable used when expanding endpoint URLs.
     */
    protected static final String API_URL = "apiUrl";

    protected final String apiUrl;
    protected final CloseableHttpClient httpClient;
    protected final ObjectMapper mapper;

    @SneakyThrows
    protected <T> T performDeleteRequest(final URIBuilder uriBuilder,
                                         final TypeReference<T> responseType) {

        URI uri = uriBuilder.build();

        HttpDelete request = new HttpDelete(uri);
        request.setHeader(ACCEPT, "application/json");

        return httpClient.execute(request, response -> {
            String body = EntityUtils.toString(response.getEntity());
            return mapper.readValue(body, responseType);
        });
    }

    @SneakyThrows
    protected <T> T performGetRequest(final URIBuilder uriBuilder,
                                      final TypeReference<T> responseType) {

        URI uri = uriBuilder.build();

        HttpGet request = new HttpGet(uri);
        request.setHeader(ACCEPT, "application/json");

        return httpClient.execute(request, response -> {
            String body = EntityUtils.toString(response.getEntity());
            return mapper.readValue(body, responseType);
        });
    }

    @SneakyThrows
    protected <T> WpPagedResponse<T> performPagingRequest(final URIBuilder uriBuilder,
                                                          final WpPaginationQuery paginationQuery,
                                                          final TypeReference<List<T>> responseType) {
        URI uri = uriBuilder.build();

        HttpGet request = new HttpGet(uri);
        request.setHeader(ACCEPT, "application/json");

        return httpClient.execute(request, response -> {
            int totalItems = ofNullable(response.getHeader("X-WP-Total"))
                    .map(header -> Integer.parseInt(header.getValue()))
                    .orElse(0);

            int totalPages = ofNullable(response.getHeader("X-WP-TotalPages"))
                    .map(header -> Integer.parseInt(header.getValue()))
                    .orElse(0);

            failOnEmptyResponseBody(response);

            String json = EntityUtils.toString(response.getEntity());
            List<T> items = mapper.readValue(json, responseType);

            return new WpPagedResponse<>(
                    items,
                    paginationQuery.pageSize(),
                    totalItems,
                    totalPages,
                    paginationQuery.pageNumber()
            );
        });
    }

    @SneakyThrows
    protected <T> T performPostWithBody(final URIBuilder uriBuilder,
                                        final Object requestBody,
                                        final TypeReference<T> responseType) {

        URI uri = uriBuilder.build();

        HttpPost request = new HttpPost(uri);
        request.setHeader(ACCEPT, "application/json");
        request.setHeader(HttpHeaders.CONTENT_TYPE, "application/json");

        String jsonBody = mapper.writeValueAsString(requestBody);
        request.setEntity(new StringEntity(jsonBody, StandardCharsets.UTF_8));

        return httpClient.execute(request, response -> {
            failOnEmptyResponseBody(response);

            String json = EntityUtils.toString(response.getEntity());
            return mapper.readValue(json, responseType);
        });
    }

    @SneakyThrows
    protected <T> T performPostWithFileUpload(final URIBuilder uriBuilder,
                                              final File file,
                                              final String fileName,
                                              final String mimeType,
                                              final TypeReference<T> responseType) {

        URI uri = uriBuilder.build();

        HttpPost request = new HttpPost(uri);
        request.setHeader(ACCEPT, "application/json");

        request.setEntity(
                MultipartEntityBuilder.create()
                                      .addBinaryBody(
                                              "file",
                                              file,
                                              ContentType.parse(mimeType),
                                              fileName
                                      )

                                      .build()
        );

        return httpClient.execute(request, response -> {
            failOnEmptyResponseBody(response);

            String json = EntityUtils.toString(response.getEntity());
            return mapper.readValue(json, responseType);
        });
    }

    protected URIBuilder urlBuilder(final String path, final Map<String, Object> pathParams) throws URISyntaxException {
        final String substituted = new StringSubstitutor(emptyIfNull(pathParams)).replace(path);
        return new URIBuilder(substituted);
    }

    protected static <K, V> Map<K, V> emptyIfNull(final Map<K, V> map) {
        return ofNullable(map).orElseGet(Collections::emptyMap);
    }

    private static void failOnEmptyResponseBody(final ClassicHttpResponse response) throws IOException {
        if (response.getEntity() == null) {
            throw new IOException("Empty response body");
        }
    }
}