RestServiceResponse.java
package com.bonitasoft.processbuilder.records;
import com.bonitasoft.processbuilder.enums.RestContentType;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
/**
* Represents a REST service response.
* <p>
* This record contains all the information from a REST API response,
* including the status code, headers, body, and execution metadata.
* </p>
*
* @param statusCode The HTTP status code
* @param headers Response headers
* @param body The response body as a string
* @param contentType The content type of the response
* @param executionTimeMs Time taken to execute the request in milliseconds
* @param errorMessage Error message if the request failed (null if successful)
* @param url The URL that was called
* @author Bonitasoft
* @since 1.0
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public record RestServiceResponse(
int statusCode,
Map<String, String> headers,
String body,
RestContentType contentType,
long executionTimeMs,
String errorMessage,
String url
) {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
/**
* Compact constructor with defaults.
*/
public RestServiceResponse {
headers = headers != null ? Map.copyOf(headers) : Collections.emptyMap();
}
// ========================================================================
// Factory Methods
// ========================================================================
/**
* Creates a successful response.
*
* @param statusCode The HTTP status code
* @param headers Response headers
* @param body Response body
* @param contentType Content type of the response
* @param executionTimeMs Execution time
* @param url The URL called
* @return A successful response
*/
public static RestServiceResponse success(
int statusCode,
Map<String, String> headers,
String body,
RestContentType contentType,
long executionTimeMs,
String url) {
return new RestServiceResponse(statusCode, headers, body, contentType, executionTimeMs, null, url);
}
/**
* Creates an error response.
*
* @param errorMessage The error message
* @param executionTimeMs Execution time
* @param url The URL that was called
* @return An error response with status code -1
*/
public static RestServiceResponse error(String errorMessage, long executionTimeMs, String url) {
return new RestServiceResponse(-1, Collections.emptyMap(), null, null, executionTimeMs, errorMessage, url);
}
/**
* Creates an error response from an exception.
*
* @param exception The exception that occurred
* @param executionTimeMs Execution time
* @param url The URL that was called
* @return An error response
*/
public static RestServiceResponse fromException(Exception exception, long executionTimeMs, String url) {
String message = exception.getMessage();
if (message == null || message.isBlank()) {
message = exception.getClass().getSimpleName();
}
return error(message, executionTimeMs, url);
}
// ========================================================================
// Status Check Methods
// ========================================================================
/**
* Checks if the response indicates success (2xx status code).
*
* @return true if status code is between 200 and 299
*/
public boolean isSuccessful() {
return statusCode >= 200 && statusCode < 300;
}
/**
* Checks if the response indicates a client error (4xx status code).
*
* @return true if status code is between 400 and 499
*/
public boolean isClientError() {
return statusCode >= 400 && statusCode < 500;
}
/**
* Checks if the response indicates a server error (5xx status code).
*
* @return true if status code is between 500 and 599
*/
public boolean isServerError() {
return statusCode >= 500 && statusCode < 600;
}
/**
* Checks if there was an error (network error or non-success HTTP status).
*
* @return true if there was an error
*/
public boolean isError() {
return errorMessage != null || statusCode < 0 || statusCode >= 400;
}
/**
* Checks if the response was a redirect (3xx status code).
*
* @return true if status code is between 300 and 399
*/
public boolean isRedirect() {
return statusCode >= 300 && statusCode < 400;
}
// ========================================================================
// Body Parsing Methods
// ========================================================================
/**
* Parses the response body as JSON.
*
* @return Optional containing the JsonNode, or empty if parsing fails
*/
public Optional<JsonNode> bodyAsJson() {
if (body == null || body.isBlank()) {
return Optional.empty();
}
try {
return Optional.of(OBJECT_MAPPER.readTree(body));
} catch (Exception e) {
return Optional.empty();
}
}
/**
* Parses the response body as a specific type.
*
* @param <T> The type to parse to
* @param clazz The class of the type
* @return Optional containing the parsed object, or empty if parsing fails
*/
public <T> Optional<T> bodyAs(Class<T> clazz) {
if (body == null || body.isBlank()) {
return Optional.empty();
}
try {
return Optional.of(OBJECT_MAPPER.readValue(body, clazz));
} catch (Exception e) {
return Optional.empty();
}
}
/**
* Gets a specific field from the JSON body.
*
* @param fieldName The field name to extract
* @return Optional containing the field value as string, or empty if not found
*/
public Optional<String> getJsonField(String fieldName) {
return bodyAsJson()
.map(json -> json.get(fieldName))
.filter(node -> !node.isNull())
.map(JsonNode::asText);
}
/**
* Checks if the response body contains JSON content.
*
* @return true if the content type is JSON and body is not empty
*/
public boolean hasJsonBody() {
return contentType != null && contentType.isJson() && body != null && !body.isBlank();
}
// ========================================================================
// Header Methods
// ========================================================================
/**
* Gets a specific header value (case-insensitive).
*
* @param headerName The header name
* @return Optional containing the header value, or empty if not found
*/
public Optional<String> getHeader(String headerName) {
if (headerName == null) {
return Optional.empty();
}
return headers.entrySet().stream()
.filter(e -> e.getKey().equalsIgnoreCase(headerName))
.map(Map.Entry::getValue)
.findFirst();
}
/**
* Gets the Location header (for redirects).
*
* @return Optional containing the Location header value
*/
public Optional<String> getLocation() {
return getHeader("Location");
}
// ========================================================================
// Utility Methods
// ========================================================================
/**
* Returns a summary of the response for logging.
*
* @return A summary string
*/
public String toSummary() {
StringBuilder summary = new StringBuilder();
summary.append("HTTP ").append(statusCode);
if (errorMessage != null) {
summary.append(" ERROR: ").append(errorMessage);
}
summary.append(" (").append(executionTimeMs).append("ms)");
if (body != null) {
summary.append(" Body: ").append(body.length()).append(" chars");
}
return summary.toString();
}
/**
* Creates a copy of this response with a different body.
*
* @param newBody The new body
* @return A new response with the updated body
*/
public RestServiceResponse withBody(String newBody) {
return new RestServiceResponse(statusCode, headers, newBody, contentType, executionTimeMs, errorMessage, url);
}
/**
* Creates a copy of this response with a different error message.
*
* @param newErrorMessage The new error message
* @return A new response with the updated error message
*/
public RestServiceResponse withError(String newErrorMessage) {
return new RestServiceResponse(statusCode, headers, body, contentType, executionTimeMs, newErrorMessage, url);
}
}