Refactor response handling and centralize JSON configuration

Replaced direct fields in `AbstractResponse` with `ResponseResultInfo` for better encapsulation and consistency. Introduced `JsonConf` to provide a shared, reusable `ObjectMapper` configuration. Updated tests and logic to align with the refactored response structure, enhancing maintainability and readability.
This commit is contained in:
2025-04-21 14:06:25 +02:00
parent af4e09f938
commit c1675d0fba
10 changed files with 325 additions and 71 deletions
+1
View File
@@ -5,6 +5,7 @@
/.factorypath
/.springBeans
/.idea
*.iml
# local files
/info.txt
@@ -2,14 +2,11 @@ package codes.thischwa.cf;
import codes.thischwa.cf.model.AbstractEntity;
import codes.thischwa.cf.model.AbstractResponse;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
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.HttpPatch;
@@ -39,7 +36,7 @@ abstract class CfBasicHttpClient {
private final ObjectMapper objectMapper;
CfBasicHttpClient(String baseUrl, String authEmail, String authKey)
throws IllegalArgumentException {
throws IllegalArgumentException {
if (authEmail == null || authEmail.isBlank()) {
throw new IllegalArgumentException("Authentication email must not be null or blank!");
}
@@ -49,110 +46,113 @@ abstract class CfBasicHttpClient {
this.baseUrl = baseUrl;
this.authEmail = authEmail;
this.authKey = authKey;
this.objectMapper = initObjectMapper();
}
private ObjectMapper initObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
return mapper;
this.objectMapper = JsonConf.initObjectMapper();
}
private CloseableHttpClient createHttpClient() {
return HttpClients.custom()
.addRequestInterceptorFirst(
(request, context, execChain) -> {
request.addHeader(HttpHeaders.ACCEPT_CHARSET, "UTF-8");
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip");
request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader(
HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader("X-Auth-Email", authEmail);
request.addHeader("X-Auth-Key", authKey);
})
.build();
return HttpClients.custom().addRequestInterceptorFirst((request, context, execChain) -> {
request.addHeader(HttpHeaders.ACCEPT_CHARSET, "UTF-8");
request.addHeader(HttpHeaders.ACCEPT_ENCODING, "gzip");
request.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.getMimeType());
request.addHeader("X-Auth-Email", authEmail);
request.addHeader("X-Auth-Key", authKey);
}).build();
}
private <T extends AbstractResponse> T executeRequest(
ClassicHttpRequest request, Class<T> responseType) throws CloudflareApiException {
private <T extends AbstractResponse> T executeRequest(ClassicHttpRequest request,
Class<T> responseType)
throws CloudflareApiException {
String logUri = null;
try (CloseableHttpClient client = createHttpClient()) {
ResultWrapper result =
client.execute(
request,
(ClassicHttpResponse response) ->
new ResultWrapper(
response.getCode(),
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)));
ResultWrapper result = client.execute(request,
(ClassicHttpResponse response) -> new ResultWrapper(response.getCode(),
EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8)));
logUri = request.getRequestUri();
if (result.statusCode >= 200 && result.statusCode < 300) {
return objectMapper.readValue(result.responseBody, responseType);
T respObj = objectMapper.readValue(result.responseBody, responseType);
if (!respObj.getResponseResultInfo().isSuccess()) {
log.error("API error.");
respObj.getResponseResultInfo().getErrors().forEach(e -> log.error(" - {}", e));
throw new CloudflareApiException("API error.");
}
return respObj;
} else {
log.error(
"{} request failed for URL {}: Status {}",
request.getMethod(),
request.getUri(),
result.statusCode);
log.error("{} request failed for URL {}: Status {}", request.getMethod(), request.getUri(),
result.statusCode);
throw new CloudflareApiException(
request.getMethod() + " request failed with status code: " + result.statusCode);
request.getMethod() + " request failed with status code: " + result.statusCode);
}
} catch (JsonProcessingException e) {
log.error("JSON parsing error for request to {}", logUri, e);
throw new CloudflareApiException("Error processing JSON response", e);
} catch (Exception e) {
log.error("Error during request execution", e);
throw new CloudflareApiException("Request failed", e);
throw new CloudflareApiException("Server error!", e);
}
}
/** Sends a GET request to the given endpoint and maps the response. */
/**
* Sends a GET request to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T getRequest(String endpoint, Class<T> responseType)
throws CloudflareApiException {
throws CloudflareApiException {
HttpGet request = new HttpGet(buildUrl(endpoint));
return executeRequest(request, responseType);
}
/** Sends a DELETE request to the given endpoint and maps the response. */
/**
* Sends a DELETE request to the given endpoint and maps the response.
*/
<T extends AbstractResponse> T deleteRequest(String endpoint) throws CloudflareApiException {
HttpDelete request = new HttpDelete(buildUrl(endpoint));
return executeRequest(request, (Class<T>) codes.thischwa.cf.model.RecordSingleResponse.class);
}
/** Sends a POST request with a payload to the given endpoint and maps the response. */
<T extends AbstractResponse, R extends AbstractEntity> T postRequest(
String endpoint, R requestPayload) throws CloudflareApiException {
/**
* Sends a POST request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse, R extends AbstractEntity> T postRequest(String endpoint,
R requestPayload)
throws CloudflareApiException {
HttpPost request = new HttpPost(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, (Class<T>) codes.thischwa.cf.model.RecordSingleResponse.class);
}
/** Sends a PUT request with a payload to the given endpoint and maps the response. */
<T extends AbstractResponse, R extends AbstractEntity> T putRequest(
String endpoint, R requestPayload, Class<T> responseType) throws CloudflareApiException {
/**
* Sends a PUT request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse, R extends AbstractEntity> T putRequest(String endpoint,
R requestPayload,
Class<T> responseType)
throws CloudflareApiException {
HttpPut request = new HttpPut(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, responseType);
}
/** Sends a PATCH request with a payload to the given endpoint and maps the response. */
<T extends AbstractResponse, R extends AbstractEntity> T patchRequest(
String endpoint, R requestPayload) throws CloudflareApiException {
/**
* Sends a PATCH request with a payload to the given endpoint and maps the response.
*/
<T extends AbstractResponse, R extends AbstractEntity> T patchRequest(String endpoint,
R requestPayload)
throws CloudflareApiException {
HttpPatch request = new HttpPatch(buildUrl(endpoint));
setRequestPayload(request, requestPayload);
return executeRequest(request, (Class<T>) codes.thischwa.cf.model.RecordSingleResponse.class);
}
/** Sets the JSON payload for a request. */
private <R extends AbstractEntity> void setRequestPayload(
BasicClassicHttpRequest request, R requestPayload) throws CloudflareApiException {
/**
* Sets the JSON payload for a request.
*/
private <R extends AbstractEntity> void setRequestPayload(BasicClassicHttpRequest request,
R requestPayload)
throws CloudflareApiException {
try {
request.setEntity(
new StringEntity(
objectMapper.writeValueAsString(requestPayload), ContentType.APPLICATION_JSON));
request.setEntity(new StringEntity(objectMapper.writeValueAsString(requestPayload),
ContentType.APPLICATION_JSON));
} catch (JsonProcessingException e) {
throw new CloudflareApiException("Error serializing JSON payload", e);
}
@@ -162,5 +162,6 @@ abstract class CfBasicHttpClient {
return baseUrl + endpoint;
}
private record ResultWrapper(int statusCode, String responseBody) {}
private record ResultWrapper(int statusCode, String responseBody) {
}
}
@@ -6,6 +6,7 @@ import codes.thischwa.cf.model.RecordEntity;
import codes.thischwa.cf.model.RecordMultipleResponse;
import codes.thischwa.cf.model.RecordSingleResponse;
import codes.thischwa.cf.model.RecordType;
import codes.thischwa.cf.model.ResponseResultInfo;
import codes.thischwa.cf.model.ZoneEntity;
import codes.thischwa.cf.model.ZoneMultipleResponse;
import java.util.List;
@@ -307,9 +308,10 @@ public class CfDnsClient extends CfBasicHttpClient {
private void checkResponse(AbstractResponse resp, boolean singleResultExpected)
throws CloudflareApiException {
if (!resp.isSuccess()) {
ResponseResultInfo resultInfo = resp.getResponseResultInfo();
if (!resultInfo.isSuccess()) {
String errors =
resp.getErrors().stream().map(Object::toString).collect(Collectors.joining(", "));
resultInfo.getErrors().stream().map(Object::toString).collect(Collectors.joining(", "));
throw new CloudflareApiException("Error in response: " + errors);
}
@@ -0,0 +1,23 @@
package codes.thischwa.cf;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
/**
* The JsonConf class provides a utility method for initializing and configuring a shared
* {@link ObjectMapper} instance for JSON serialization and deserialization.
*/
class JsonConf {
static ObjectMapper initObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
return mapper;
}
}
@@ -1,6 +1,6 @@
package codes.thischwa.cf.model;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.Data;
/**
@@ -22,7 +22,7 @@ import lombok.Data;
*/
@Data
public abstract class AbstractResponse {
private boolean success;
private List<String> errors;
private List<String> messages;
@JsonUnwrapped
private ResponseResultInfo responseResultInfo;
}
@@ -0,0 +1,22 @@
package codes.thischwa.cf.model;
import java.util.List;
import lombok.Data;
/**
* Represents the result of a response with metadata about its success and associated messages or
* errors.
* <p>This class provides a structure to capture the outcome of an operation, including:
* <ul>
* <li>Whether the operation was successful.
* <li>A list of error messages if the operation failed.
* <li>A list of informational or success messages.
* </ul>
* It can be used to standardize the response format in an application.
*/
@Data
public class ResponseResultInfo {
private boolean success;
private List<String> errors;
private List<String> messages;
}
@@ -0,0 +1,20 @@
package codes.thischwa.cf;
import codes.thischwa.cf.model.ZoneMultipleResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
public class ObjectMapperTest {
@Test
void testObjectMapper() throws IOException {
ObjectMapper mapper = JsonConf.initObjectMapper();
ZoneMultipleResponse resp =
mapper.readValue(this.getClass().getResourceAsStream("/zone-list-response.json"),
ZoneMultipleResponse.class);
assertNotNull(resp.getResponseResultInfo());
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"result": null,
"success": false,
"errors": [
{
"code": 81053,
"message": "An A, AAAA, or CNAME record with that host already exists. For more details, refer to \u003chttps://developers.cloudflare.com/dns/manage-dns-records/troubleshooting/records-with-same-name/\u003e."
}
],
"messages": []
}
+1 -1
View File
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<configuration>
<appender name="current"
class="ch.qos.logback.core.ConsoleAppender">
+174
View File
@@ -0,0 +1,174 @@
{
"result": [
{
"id": "0a83dd6e7f8c46039f2517bbded8115e",
"name": "mein-d-ns.de",
"status": "active",
"paused": false,
"type": "full",
"development_mode": -230181,
"name_servers": [
"blair.ns.cloudflare.com",
"sergi.ns.cloudflare.com"
],
"original_name_servers": [
"c.ns14.net",
"b.ns14.net",
"a.ns14.net",
"d.ns14.net"
],
"original_registrar": null,
"original_dnshost": null,
"modified_on": "2025-01-20T09:06:52.122538Z",
"created_on": "2025-01-20T08:56:34.736214Z",
"activated_on": "2025-01-20T09:06:52.122538Z",
"meta": {
"step": 2,
"custom_certificate_quota": 0,
"page_rule_quota": 3,
"phishing_detected": false
},
"owner": {
"id": null,
"type": "user",
"email": null
},
"account": {
"id": "563335e3d73549dbbc0620f4ce82d527",
"name": "myAccount"
},
"tenant": {
"id": null,
"name": null
},
"tenant_unit": {
"id": null
},
"permissions": [
"#image:read",
"#image:edit",
"#worker:edit",
"#analytics:read",
"#ssl:edit",
"#zone_settings:read",
"#organization:edit",
"#waf:read",
"#waf:edit",
"#logs:read",
"#member:read",
"#worker:read",
"#blocks:read",
"#blocks:edit",
"#access:read",
"#access:edit",
"#billing:read",
"#teams:read",
"#organization:read",
"#logs:edit",
"#zaraz:publish",
"#zone_settings:edit",
"#fbm:read",
"#subscription:read",
"#lb:read",
"#waitingroom:edit",
"#ssl:read",
"#subscription:edit",
"#dns_records:read",
"#dns_records:edit",
"#api_gateway:read",
"#api_gateway:edit",
"#cds_compute_account:edit",
"#ces_submissions:read",
"#fbm:edit",
"#healthchecks:edit",
"#stream:edit",
"#lb:edit",
"#healthchecks:read",
"#billing:edit",
"#member:edit",
"#cfone_read",
"#cfone_edit",
"#integration:read",
"#zaraz:edit",
"#magic:read",
"#magic:edit",
"#zaraz:read",
"#stream:read",
"#integration:install",
"#dash_sso:edit",
"#zone:read",
"#http_applications:read",
"#http_applications:edit",
"#ces_analytics:read",
"#resilience:edit",
"#teams:edit",
"#r2_bucket_warehouse:read",
"#r2_bucket_warehouse:edit",
"#query_cache:read",
"#query_cache:edit",
"#dex:read",
"#zone_versioning:read",
"#zone_versioning:edit",
"#cds:read",
"#ces_search:action",
"#ces_search:read",
"#ces_search:preview",
"#ces_search:raw",
"#ces_search:trace",
"#fbm_acc:edit",
"#teams:pii",
"#r2_bucket_item:read",
"#r2_bucket_item:edit",
"#integration:edit",
"#resilience:read",
"#zone:edit",
"#dash_sso:read",
"#legal:read",
"#ces_integration:edit",
"#ces_integration:read",
"#cache_purge:edit",
"#vectorize:read",
"#vectorize:edit",
"#auditlogs:read",
"#teams:report",
"#ces_settings:edit",
"#ces_settings:read",
"#waitingroom:read",
"#web3:read",
"#web3:edit",
"#dex:edit",
"#cds_compute_account:read",
"#cds:edit",
"#r2_bucket:read",
"#r2_bucket:edit",
"#ces_phishguard:read",
"#page_shield:read",
"#page_shield:edit",
"#legal:edit",
"#app:edit"
],
"plan": {
"id": "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"name": "Free Website",
"price": 0,
"currency": "USD",
"frequency": "",
"is_subscribed": false,
"can_subscribe": false,
"legacy_id": "free",
"legacy_discount": false,
"externally_managed": false
}
}
],
"result_info": {
"page": 1,
"per_page": 20,
"total_pages": 1,
"count": 1,
"total_count": 1
},
"success": true,
"errors": [],
"messages": []
}