au.net.causal.shoelaces.jersey.common.AbstractJacksonDateTimeParamConverterProvider Maven / Gradle / Ivy
package au.net.causal.shoelaces.jersey.common;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ParamConverter;
import jakarta.ws.rs.ext.ParamConverterProvider;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.Set;
/**
* Param converter that uses Jackson for parsing date and time types.
*
*
* This class is abstract - subclasses supply a Jackson object mapper that is used for value conversion.
*
* TODO describe how format can be customized with @JsonFormat
*/
public abstract class AbstractJacksonDateTimeParamConverterProvider implements ParamConverterProvider
{
private static final Set> DEFAULT_HANDLED_TYPES = Set.of(
LocalDate.class,
LocalTime.class,
LocalDateTime.class,
ZonedDateTime.class,
OffsetDateTime.class,
OffsetTime.class,
Instant.class,
YearMonth.class,
MonthDay.class,
Year.class,
Period.class,
Duration.class,
ZoneId.class,
ZoneOffset.class
);
private final Set> handledTypes;
protected AbstractJacksonDateTimeParamConverterProvider(Set> handledTypes)
{
this.handledTypes = Set.copyOf(handledTypes);
}
protected AbstractJacksonDateTimeParamConverterProvider()
{
this(DEFAULT_HANDLED_TYPES);
}
/**
* Obtains a Jackson object mapper suitable for converting a type.
*
*
* Typically, subclasses will just use a single object mapper for all types, however sometimes it might be desirable to override the object mapper for
* a particular type of parameter configured with certain annotations.
*
* @param rawType the raw type of the object to be converted.
* @param genericType the type of object to be converted. E.g. if an String value
* representing the injected request parameter
* is to be converted into a method parameter, this will be the
* formal type of the method parameter as returned by {@code Class.getGenericParameterTypes}.
* @param annotations an array of the annotations associated with the convertible
* parameter instance. E.g. if a string value is to be converted into a method parameter,
* this would be the annotations on that parameter as returned by
* {@link java.lang.reflect.Method#getParameterAnnotations}.
*
* @return a Jackson object mapper that is used for value conversion.
*/
protected abstract ObjectMapper objectMapper(Class> rawType, Type genericType, Annotation[] annotations);
@Override
public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations)
{
if (handledTypes.contains(rawType))
{
ObjectMapper objectMapper = objectMapper(rawType, genericType, annotations);
//Attempt to find JsonFormat annotation
JsonFormat jsonFormatAnnotation = findFirstInstance(annotations, JsonFormat.class);
if (jsonFormatAnnotation != null)
{
//Don't modify original object mapper if it is being reconfigured
objectMapper = objectMapper.copy();
objectMapper.configOverride(rawType).setFormat(new JsonFormat.Value(jsonFormatAnnotation));
}
final ObjectMapper fObjectMapper = objectMapper;
return new ParamConverter<>()
{
@Override
public T fromString(String value)
{
//First attempt to parse value as string
try
{
return fObjectMapper.convertValue(value, rawType);
}
catch (IllegalArgumentException e)
{
//Could not parse as string, try to parse as JSON (maybe a number / timestamp?)
try
{
return fObjectMapper.readValue(value, rawType);
}
catch (JsonProcessingException ex)
{
//Parse as raw JSON failed as well, throw original error but add this one as suppressed
e.addSuppressed(ex);
throw createExceptionFromJsonError(value, e);
}
}
}
@Override
public String toString(T value)
{
try
{
return fObjectMapper.writeValueAsString(value);
}
catch (JsonProcessingException e)
{
throw new RuntimeException(e);
}
}
};
}
//Param converter can't handle this type
return null;
}
/**
* Finds the first object in an array of supertypes that implements/extends a target type.
*
* @param values an array of values.
* @param targetType search for an object of this type in the array of values.
* @param element type of the array to search.
* @param target type to search for.
*
* @return the first element in the values array that is of the target type, or null if none was found.
*/
static T findFirstInstance(A[] values, Class targetType)
{
for (A value : values)
{
if (targetType.isInstance(value))
return targetType.cast(value);
}
return null;
}
/**
* Converts a JSON processing/parsing error into a JAX-RS or other runtime error.
*
*
* By default, this method generates a JAX-RS web application exception subclass with an HTTP bad request error code. Subclasses may override this method to customize the
* error code or other aspects of the exception when JSON parsing fails.
*
* @param e the JSON error to convert.
*
* @return a JAX-RS exception.
*
* @see WebApplicationParamParsingException
*/
protected RuntimeException createExceptionFromJsonError(String rawParameterValue, Exception e)
{
throw new WebApplicationParamParsingException(e, Response.Status.BAD_REQUEST, rawParameterValue);
}
}