Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
se.fortnox.reactivewizard.client.HttpClient Maven / Gradle / Ivy
package se.fortnox.reactivewizard.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.StreamReadConstraints;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.collect.Sets;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.timeout.ReadTimeoutException;
import jakarta.inject.Inject;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;
import se.fortnox.reactivewizard.jaxrs.ByteBufCollector;
import se.fortnox.reactivewizard.jaxrs.FieldError;
import se.fortnox.reactivewizard.jaxrs.JaxRsMeta;
import se.fortnox.reactivewizard.jaxrs.RequestLogger;
import se.fortnox.reactivewizard.jaxrs.WebException;
import se.fortnox.reactivewizard.metrics.HealthRecorder;
import se.fortnox.reactivewizard.metrics.Metrics;
import se.fortnox.reactivewizard.util.FluxRxConverter;
import se.fortnox.reactivewizard.util.JustMessageException;
import se.fortnox.reactivewizard.util.ReactiveDecorator;
import se.fortnox.reactivewizard.util.ReflectionUtil;
import javax.ws.rs.BeanParam;
import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.FormParam;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpResponseStatus.GATEWAY_TIMEOUT;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
import static javax.ws.rs.core.HttpHeaders.CONTENT_TYPE;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
import static reactor.core.Exceptions.isRetryExhausted;
import static reactor.core.publisher.Mono.just;
public class HttpClient implements InvocationHandler {
private static final Logger LOG = LoggerFactory.getLogger(HttpClient.class);
private static final Class BYTEARRAY_TYPE = (new byte[0]).getClass();
private static final String COOKIE = "Cookie";
private static final String QUOTE = "\"";
private static final String ERROR_CALLING_OTHER_SERVICE = """
Error calling other service:
\tResponse Status: %d
\tURL: %s
\tRequest Headers: %s
\tResponse Headers: %s
\tData: %s""";
protected final InetSocketAddress serverInfo;
protected final HttpClientConfig config;
private final ByteBufCollector collector;
private final RequestParameterSerializers requestParameterSerializers;
private final Set preRequestHooks;
private final ReactorRxClientProvider clientProvider;
private final ObjectMapper objectMapper;
private final RequestLogger requestLogger;
private final Map, List> beanParamCache = new ConcurrentHashMap<>();
private final Map jaxRsMetaMap = new ConcurrentHashMap<>();
private int timeout = 10;
private TemporalUnit timeoutUnit = ChronoUnit.SECONDS;
private final Duration retryDuration;
@Inject
public HttpClient(HttpClientConfig config,
ReactorRxClientProvider clientProvider,
ObjectMapper objectMapper,
RequestParameterSerializers requestParameterSerializers,
Set preRequestHooks,
RequestLogger requestLogger
) {
this.config = config;
this.clientProvider = clientProvider;
this.objectMapper = objectMapper;
this.requestLogger = requestLogger;
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
var constraints = StreamReadConstraints.builder()
.maxStringLength(20_000_000)
.build();
this.objectMapper.getFactory()
.setStreamReadConstraints(constraints);
this.requestParameterSerializers = requestParameterSerializers;
serverInfo = InetSocketAddress.createUnresolved(config.getHost(), config.getPort());
collector = new ByteBufCollector(config.getMaxResponseSize());
this.preRequestHooks = preRequestHooks;
this.retryDuration = Duration.ofMillis(config.getRetryDelayMs());
}
public HttpClient(HttpClientConfig config) {
this(config, new ReactorRxClientProvider(config, new HealthRecorder()), new ObjectMapper(), new RequestParameterSerializers(),
emptySet(), new RequestLogger());
}
public static void setTimeout(Object proxy, int timeout, ChronoUnit timeoutUnit) {
ifHttpClientDo(proxy, httpClient -> httpClient.setTimeout(timeout, timeoutUnit));
}
public void setTimeout(int timeout, ChronoUnit timeoutUnit) {
this.timeout = timeout;
this.timeoutUnit = timeoutUnit;
}
public static void markHeaderAsSensitive(Object proxy, String header) {
markHeadersAsSensitive(proxy, singleton(header));
}
public static void markHeadersAsSensitive(Object proxy, Set headers) {
ifHttpClientDo(proxy, httpClient -> httpClient.addSensitiveHeaders(headers));
}
private static void ifHttpClientDo(Object proxy, Consumer consumer) {
if (Proxy.isProxyClass(proxy.getClass())) {
Object handler = Proxy.getInvocationHandler(proxy);
if (handler instanceof HttpClient httpClient) {
consumer.accept(httpClient);
}
}
}
public void addSensitiveHeaders(Set headers) {
headers.forEach(requestLogger::addRedactedHeaderClient);
}
@SuppressWarnings("unchecked")
public T create(Class jaxRsInterface) {
return (T) Proxy.newProxyInstance(jaxRsInterface.getClassLoader(), new Class[]{jaxRsInterface}, this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) {
if (arguments == null) {
arguments = new Object[0];
}
RequestBuilder request = createRequest(method, arguments);
addDevOverrides(request);
addAuthenticationHeaders(request);
reactor.netty.http.client.HttpClient rxClient = clientProvider.clientFor(request.getServerInfo());
Mono response = request.submit(rxClient, request);
Class> returnType = method.getReturnType();
Mono>> responseWithResult = createResponseWithResult(method, request, response);
Flux> resultOnly = responseWithResult.flatMapMany(Response::getBody);
Function converter = FluxRxConverter.converterFromFlux(returnType);
return ReactiveDecorator.decorated(converter.apply(resultOnly), responseWithResult);
}
private Mono>> createResponseWithResult(Method method, RequestBuilder request, Mono responseMono) {
boolean isSingle = FluxRxConverter.isSingleType(method.getReturnType());
Mono>> result = responseMono.flatMap(response -> {
Mono>> error = handleError(request, response);
if (error != null) {
return error;
}
Flux body;
if (isSingle) {
body = parseResponseSingle(method, response);
} else {
body = parseResponseStream(method, response);
}
body = body.onErrorResume(e -> convertError(request, e));
return Mono.just(new Response<>(response.getHttpClientResponse(), body));
});
return withRetry(request, measure(request, result).timeout(Duration.of(timeout, timeoutUnit)))
.onErrorResume(e -> convertError(request, e));
}
private static Mono> flattenResponse(Mono>> responseFlux) {
return responseFlux.flatMap(response -> response.getBody()
.singleOrEmpty()
.map(response::withBody)
.switchIfEmpty(Mono.fromCallable(response::withNoBody))
);
}
/**
* Should be used with a Flux coming directly from another api-call to get access to metadata, such as status and header.
*
* @param source the source observable, must be observable returned from api call
* @param the type of data that should be returned in the call
* @return an observable that along with the data passes the response object from netty
*/
public static Mono>> getFullResponse(Flux source) {
Optional>>> responseFlux = ReactiveDecorator.getDecoration(source);
if (responseFlux.isEmpty()) {
throw new IllegalArgumentException("Must be used with Flux returned from api call");
}
return responseFlux.get();
}
/**
* Should be used with a Mono coming directly from another api-call to get access to metadata, such as status and header.
*
* @param source the source observable, must be observable returned from api call
* @param the type of data that should be returned in the call
* @return an observable that along with the data passes the response object from netty
*/
public static Mono> getFullResponse(Mono source) {
Optional>>> responseFlux = ReactiveDecorator.getDecoration(source);
if (responseFlux.isEmpty()) {
throw new IllegalArgumentException("Must be used with Mono returned from api call");
}
return flattenResponse(responseFlux.get());
}
private Mono convertError(RequestBuilder fullReq, Throwable throwable) {
String request = format("%s, headers: %s", fullReq.getFullUrl(), requestLogger.getHeaderValuesOrRedactClient(fullReq.getHeaders()));
LOG.warn("Failed request. Url: {}", request, throwable);
if (isRetryExhausted(throwable)) {
throwable = throwable.getCause();
}
if (throwable instanceof TimeoutException || throwable instanceof ReadTimeoutException) {
String message = format("Timeout after %d ms calling %s", Duration.of(timeout, timeoutUnit).toMillis(), request);
return Mono.error(new WebException(GATEWAY_TIMEOUT, new JustMessageException(message), false));
} else if (!(throwable instanceof WebException)) {
String message = format("Error calling %s", request);
return Mono.error(new WebException(INTERNAL_SERVER_ERROR, new JustMessageException(message, throwable), false));
}
return Mono.error(throwable);
}
protected Flux parseResponseSingle(Method method, RwHttpClientResponse response) {
if (expectsByteArrayResponse(method)) {
return Flux.from(collector.collectBytes(response.getContent()));
}
return Flux.from(collector.collectString(response.getContent())
.flatMap(stringContent -> this.deserialize(method, stringContent)));
}
private String resolveContentType(Method method, RwHttpClientResponse response) {
String contentType = Optional.ofNullable(response.getHttpClientResponse().responseHeaders().get(CONTENT_TYPE))
.map(HttpUtil::getMimeType)
.map(CharSequence::toString)
.orElse(null);
// Override response content-type if resource method is annotated with a non-empty @Produces
JaxRsMeta jaxRsMeta = new JaxRsMeta(method);
String overridingContentType = jaxRsMeta.getProduces();
if (!isNullOrEmpty(overridingContentType) && jaxRsMeta.isProducesAnnotationPresent()) {
if (!overridingContentType.equals(contentType)) {
LOG.warn("Content-Type {} does not match the Content-Type {} when parsing response stream from {}::{}, continuing with Content-Type {}",
contentType, overridingContentType, method.getDeclaringClass().getCanonicalName(), method.getName(), overridingContentType);
}
contentType = overridingContentType;
}
return contentType;
}
protected Flux parseResponseStream(Method method, RwHttpClientResponse response) {
String contentType = resolveContentType(method, response);
if (APPLICATION_JSON.equals(contentType)) {
JsonArrayDeserializer deserializer = new JsonArrayDeserializer(objectMapper, method);
return response.getContent().asByteArray().concatMap(deserializer::process);
} else {
return response.getContent().asByteArray().cast(Object.class);
}
}
private boolean expectsByteArrayResponse(Method method) {
Type type = ReflectionUtil.getTypeOfFluxOrMono(method);
return type.equals(BYTEARRAY_TYPE);
}
private void addDevOverrides(RequestBuilder fullRequest) {
if (config.getDevServerInfo() != null) {
fullRequest.setServerInfo(config.getDevServerInfo());
}
if (config.getDevCookie() != null) {
String cookie = fullRequest.getHeaders().get(COOKIE) + ";" + config.getDevCookie();
fullRequest.getHeaders().remove(COOKIE);
fullRequest.addHeader(COOKIE, cookie);
}
if (config.getDevHeaders() != null) {
config.getDevHeaders().forEach(fullRequest::addHeader);
}
}
/**
* Add Authorization-headers if the config contains username and password.
*/
private void addAuthenticationHeaders(RequestBuilder request) {
if (config.getBasicAuth() == null) {
return;
}
String basicAuthString = createBasicAuthString();
request.addHeader("Authorization", basicAuthString);
}
/**
* Create basic auth string based on config.
*
* @return the auth string
*/
private String createBasicAuthString() {
Charset charset = StandardCharsets.ISO_8859_1;
String authString = config.getBasicAuth().getUsername() + ":" + config.getBasicAuth().getPassword();
byte[] encodedAuth = Base64.getEncoder().encode(authString.getBytes(charset));
return "Basic " + new String(encodedAuth);
}
protected Mono measure(RequestBuilder fullRequest, Mono output) {
return Metrics.get("OUT_res:" + fullRequest.getKey()).measure(output);
}
protected Mono>> withRetry(RequestBuilder fullReq, Mono>> responseMono) {
return responseMono.retryWhen(Retry.backoff(config.getRetryCount(), this.retryDuration).filter(throwable -> {
if (fullReq.getHttpMethod().equals(POST)) {
// Don't retry if it was a POST, as it is not idempotent
return false;
}
if (throwable instanceof TimeoutException) {
return false;
}
Throwable cause = throwable.getCause();
if (throwable instanceof JsonMappingException || cause instanceof JsonMappingException) {
// Do not retry when deserialization failed
return false;
}
if (!(throwable instanceof WebException)) {
return true;
}
if (((WebException) throwable).getStatus().code() >= 500) {
// Log the error on every retry.
LOG.info(format("Will retry because an error occurred. %s, headers: %s",
fullReq.getFullUrl(),
requestLogger.getHeaderValuesOrRedactClient(fullReq.getHeaders())), throwable);
// Retry if it's 500+ error
return true;
}
// Don't retry if it is a 400 error or something like that
return false;
}));
}
protected void addContent(Method method, Object[] arguments, RequestBuilder requestBuilder) {
if (!requestBuilder.canHaveBody() || requestBuilder.hasContent()) {
return;
}
Class>[] types = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
StringBuilder output = new StringBuilder();
for (int i = 0; i < types.length; i++) {
Object value = arguments[i];
if (value == null) {
continue;
}
FormParam formParam = getFormParam(annotations[i]);
if (formParam != null) {
addFormParamToOutput(output, value, formParam);
requestBuilder.getHeaders().put(CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED);
} else if (isBodyArg(types[i], annotations[i])) {
try {
if (!requestBuilder.getHeaders().containsKey(CONTENT_TYPE)) {
requestBuilder.getHeaders().put(CONTENT_TYPE, APPLICATION_JSON);
}
if (requestBuilder.getHeaders().get(CONTENT_TYPE).startsWith(APPLICATION_JSON)) {
requestBuilder.setContent(objectMapper.writeValueAsBytes(value));
} else {
if (value instanceof String string) {
requestBuilder.setContent(string);
return;
} else if (value instanceof byte[] bytes) {
requestBuilder.setContent(bytes);
return;
} else if (value instanceof Publisher publisher) {
requestBuilder.setContent(publisher);
return;
}
throw new IllegalArgumentException("When content type is not " + APPLICATION_JSON
+ " the body param must be String, byte[] or Publisher extends byte[]>, but was "
+ value.getClass());
}
return;
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
if (!output.isEmpty()) {
requestBuilder.setContent(output.toString());
}
}
protected void addFormParamToOutput(StringBuilder output, Object value, FormParam formParam) {
if (!output.isEmpty()) {
output.append("&");
}
output.append(formParam.value()).append("=").append(urlEncode(value.toString()));
}
private FormParam getFormParam(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof FormParam formParam) {
return formParam;
}
}
return null;
}
protected boolean isBodyArg(@SuppressWarnings("unused") Class> cls, Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation instanceof QueryParam || annotation instanceof PathParam || annotation instanceof HeaderParam || annotation instanceof CookieParam) {
return false;
}
}
return requestParameterSerializers.getSerializer(cls) == null;
}
protected Mono>> handleError(RequestBuilder request, RwHttpClientResponse response) {
HttpResponseStatus status = response.getHttpClientResponse().status();
if (status.code() >= 400) {
return collector.collectString(response.getContent()).onErrorReturn("").map(data -> {
String message = format(ERROR_CALLING_OTHER_SERVICE,
status.code(),
request.getFullUrl(),
requestLogger.getHeaderValuesOrRedactClient(request.getHeaders()),
formatHeaders(response.getHttpClientResponse()),
data);
Throwable detailedErrorCause = new HttpClient.ThrowableWithoutStack(message);
HttpClient.DetailedError detailedError = getDetailedError(data, detailedErrorCause);
String reasonPhrase = detailedError.hasReason() ? detailedError.reason() : status.reasonPhrase();
HttpResponseStatus responseStatus = new HttpResponseStatus(status
.code(),
reasonPhrase);
throw new WebException(responseStatus, detailedError, false, data);
});
}
return null;
}
private String formatHeaders(reactor.netty.http.client.HttpClientResponse clientResponse) {
StringBuilder headers = new StringBuilder();
clientResponse.responseHeaders().forEach(h -> headers.append(h.getKey()).append('=').append(h.getValue()).append(' '));
return headers.toString();
}
private HttpClient.DetailedError getDetailedError(String data, Throwable cause) {
HttpClient.DetailedError detailedError = new HttpClient.DetailedError(cause);
if (data != null && !data.isEmpty()) {
try {
objectMapper.readerForUpdating(detailedError).readValue(data);
} catch (IOException e) {
detailedError.setMessage(data);
}
}
return detailedError;
}
protected JaxRsMeta getJaxRsMeta(Method method) {
return jaxRsMetaMap.computeIfAbsent(method, JaxRsMeta::new);
}
protected RequestBuilder createRequest(Method method, Object[] arguments) {
JaxRsMeta meta = getJaxRsMeta(method);
RequestBuilder request = new RequestBuilder(serverInfo, meta.getHttpMethod(), meta.getFullPath());
request.setUri(getPath(method, arguments, meta));
setHeaderParams(request, method, arguments);
addCustomParams(request, method, arguments);
Consumes consumes = method.getAnnotation(Consumes.class);
if (consumes != null && consumes.value().length != 0) {
request.addHeader("Content-Type", consumes.value()[0]);
}
applyPreRequestHooks(request);
addContent(method, arguments, request);
return request;
}
private void applyPreRequestHooks(RequestBuilder request) {
preRequestHooks.forEach(hook -> hook.apply(request));
}
@SuppressWarnings("unchecked")
private void addCustomParams(RequestBuilder request, Method method, Object[] arguments) {
Class>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
RequestParameterSerializer serializer = requestParameterSerializers.getSerializer(types[i]);
if (serializer != null) {
serializer.addParameter(arguments[i], request);
}
}
}
private void setHeaderParams(RequestBuilder request, Method method, Object[] arguments) {
Class>[] types = method.getParameterTypes();
Annotation[][] annotations = method.getParameterAnnotations();
for (int i = 0; i < types.length; i++) {
Object value = arguments[i];
if (value == null) {
continue;
}
for (Annotation annotation : annotations[i]) {
if (annotation instanceof HeaderParam headerParam) {
request.addHeader(headerParam.value(), serialize(value));
} else if (annotation instanceof CookieParam cookieParam) {
final String currentCookieValue = request.getHeaders().get(COOKIE);
final String cookiePart = cookieParam.value() + "=" + serialize(value);
if (currentCookieValue != null) {
request.addHeader(COOKIE, format("%s; %s", currentCookieValue, cookiePart));
} else {
request.addHeader(COOKIE, cookiePart);
}
}
}
}
if (isNullOrEmpty(request.getHeaders().get("Host"))) {
request.addHeader("Host", this.config.getHost());
}
}
protected Mono deserialize(Method method, String string) {
if (string == null || string.isEmpty()) {
return Mono.empty();
}
Type type = ReflectionUtil.getTypeOfFluxOrMono(method);
if (Void.class.equals(type)) {
return Mono.empty();
}
if (String.class.equals(type) && !string.startsWith(QUOTE) && !"null".equalsIgnoreCase(string)) {
return just(string);
}
try {
JavaType javaType = TypeFactory.defaultInstance().constructType(type);
ObjectReader reader = objectMapper.readerFor(javaType);
Object value = reader.readValue(string);
return Mono.justOrEmpty(value);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
protected String encode(String path) {
try {
return new URI(null, null, path, null, null).toASCIIString().replaceAll("\\+", "%2B");
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
protected String urlEncode(String path) {
return URLEncoder.encode(path, StandardCharsets.UTF_8);
}
protected String getPath(Method method, Object[] arguments, JaxRsMeta meta) {
String path = meta.getFullPath();
StringBuilder query = null;
Class>[] types = method.getParameterTypes();
List args = new ArrayList<>(asList(arguments));
List argumentAnnotations = new ArrayList<>(asList(method.getParameterAnnotations()));
for (int i = 0; i < args.size(); i++) {
Object value = args.get(i);
for (Annotation annotation : argumentAnnotations.get(i)) {
if (annotation instanceof QueryParam queryParam) {
if (value == null) {
continue;
}
if (query == null) {
query = new StringBuilder(path.contains("?") ? "&" : "?");
} else {
query.append('&');
}
query.append(queryParam.value());
query.append('=');
query.append(urlEncode(serialize(value)));
} else if (annotation instanceof PathParam pathParam) {
if (value == null) {
throw new IllegalArgumentException(
format("Failed to send http request, unexpected null argument for path param '%s' when calling %s::%s",
pathParam.value(),
method.getDeclaringClass().getCanonicalName(),
method.getName()));
}
if (path.contains("{" + pathParam.value() + ":.*}")) {
path = path.replaceAll("\\{" + pathParam.value() + ":.*\\}", this.encode(this.serialize(value)));
} else {
path = path.replaceAll("\\{" + pathParam.value() + "\\}", this.urlEncode(this.serialize(value)));
}
} else if (annotation instanceof BeanParam) {
if (value == null) {
continue;
}
beanParamCache
.computeIfAbsent(types[i], this::getBeanParamGetters)
.forEach(beanParamProperty -> {
args.add(beanParamProperty.getter.apply(value));
argumentAnnotations.add(beanParamProperty.annotations);
});
}
}
}
if (query != null) {
return path + query;
}
return path;
}
private List getBeanParamGetters(Class beanParamType) {
List result = new ArrayList<>();
for (Field field : getDeclaredFieldsFromClassAndAncestors(beanParamType)) {
Optional> optionalGetter = ReflectionUtil.getter(beanParamType, field.getName());
optionalGetter.ifPresent(getter ->
result.add(new BeanParamProperty(
getter,
field.getAnnotations()
)));
}
return result;
}
/**
* Recursive function getting all declared fields from the passed in class and its ancestors.
*
* @param clazz the clazz fetching fields from
* @return set of fields
*/
private static Set getDeclaredFieldsFromClassAndAncestors(Class clazz) {
final HashSet declaredFields = new HashSet<>(asList(clazz.getDeclaredFields()));
if (clazz.getSuperclass() == null || Object.class.equals(clazz.getSuperclass())) {
return declaredFields;
}
return Sets.union(getDeclaredFieldsFromClassAndAncestors(clazz.getSuperclass()), declaredFields);
}
protected String serialize(Object value) {
if (value instanceof Date date) {
return String.valueOf(date.getTime());
}
if (value.getClass().isArray()) {
value = asList((Object[]) value);
}
if (value instanceof List list) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < list.size(); i++) {
Object entryValue = list.get(i);
stringBuilder.append(entryValue);
if (i < list.size() - 1) {
stringBuilder.append(",");
}
}
return stringBuilder.toString();
}
return value.toString();
}
protected boolean isNullOrEmpty(String string) {
return string == null || string.isEmpty();
}
public static class DetailedError extends Throwable {
private int code;
private String error;
private String message;
private FieldError[] fields;
public DetailedError() {
this(null);
}
public DetailedError(Throwable cause) {
super(null, cause, false, false);
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String reason() {
return error;
}
public boolean hasReason() {
return code == 0 && error != null;
}
public FieldError[] getFields() {
return fields;
}
public void setFields(FieldError[] fields) {
this.fields = fields;
}
}
public static class ThrowableWithoutStack extends Throwable {
public ThrowableWithoutStack(String message) {
super(message, null, false, false);
}
}
private static class BeanParamProperty {
final Function getter;
final Annotation[] annotations;
public BeanParamProperty(Function getter, Annotation[] annotations) {
this.getter = getter;
this.annotations = annotations;
}
}
}