Konwertery parametrów żądania dla JAX-RS
JAX-RS bardzo mocno usprawnia pracę z architekturą REST, pozwalając m.in. na wstrzykiwanie parametrów żądania do metody kontrolera (m.in. dzięki adnotacji @QueryParam
).
Pozwala to na automatyczną konwersję przysyłanych danych do większości używanych prostych typów.
Co w przypadku, gdy zależy nam na konwersji nieobsługiwanego typu?
Żaden problem, wystarczy napisać własny konwerter.
Aby zobrazować problem, stwórzmy prosty kontroler zwracający datę z dnia następującego po przekazanej jako parametr GET
.
Aby uprościć sobie zadanie, użyjmy typu java.time.LocalDate
z Javy w wersji 8:
import java.time.LocalDate;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
@Path(value = "/")
public class RestController {
@GET
@Path(value = "tomorrow")
@Produces(MediaType.APPLICATION_JSON)
public LocalDate getTomorrow(@QueryParam(value = "today") LocalDate today) {
return today.plusDays(1);
}
}
Efektem uruchomienia aplikacji z tym kontrolerem będzie wyjątek:
RESTEASY003875: Unable to find a constructor that takes a String param or a valueOf() or fromString() method for javax.ws.rs.QueryParam("today") on public java.time.LocalDate pl.wercia.example.jaxrs.controller.RestController.getTomorrow(java.time.LocalDate) for basetype: java.time.LocalDate
W przypadku klasy pisanej przez siebie, wystarczyłoby dodać konstruktor z parametrem typu String
lub jedną z metod statycznych valueOf(String value)
albo fromString(String value)
.
Jeśli używamy typów wbudowanych lub z zewnętrznych bibliotek, a chcielibyśmy uniknąć opakowywania w dodatkowy typ, wtedy na ratunek przychodzą konwertery parametrów.
Napisanie konwertera sprowadza się do implementacji interfejsu javax.ws.rs.ext.ParamConverter
:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import javax.ws.rs.ext.ParamConverter;
public class LocalDateParamConverter implements ParamConverter<LocalDate> {
@Override
public LocalDate fromString(String localDateString) {
return LocalDate.parse(localDateString);
}
@Override
public String toString(LocalDate localDate) {
return DateTimeFormatter.ISO_DATE.format(localDate);
}
}
Należy przy tym pamiętać, że samo napisanie konwertera, jeszcze nie rozwiązuje problemu - kontener nadal nie wie, że powinien go użyć w przypadku konkretnego typu.
Aby wskazać to jawnie, wystarczy zaimplementować interfejs javax.ws.rs.ext.ParamConverterProvider
zaadnotowany za pomocą javax.ws.rs.ext.Provider
:
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.util.Objects;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.Provider;
@Provider
public class LocalDateParamConverterProvider implements ParamConverterProvider {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
if (Objects.equals(rawType, LocalDate.class)) {
return (ParamConverter<T>) new LocalDateParamConverter();
}
return null;
}
}
Dzięki temu, po uruchomieniu aplikacji i wysłaniu żądania GET http://localhost:8080/example-jax-rs-query-converter/rest/tomorrow?today=2018-08-29
uzyskamy w odpowiedzi zserializowany obiekt java.time.LocalDate
reprezentujący datę 30.08.2018:
{
"year": 2018,
"month": "AUGUST",
"era": "CE",
"chronology": {
"calendarType": "iso8601",
"id": "ISO"
},
"dayOfYear": 242,
"dayOfWeek": "THURSDAY",
"leapYear": false,
"dayOfMonth": 30,
"monthValue": 8
}
Przykładowy projekt: