JAXB: własny adapter
Pisząc REST API z wykorzystaniem JAX-RS nie da się obejść bez serializacji i deserializacji danych z żądania i dla odpowiedzi. W większości wypadków wystarczająca jest domyślna implementacja, która nie wymaga żadnej dodatkowej konfiguracji. Schody pojawiają się wtedy, gdy chcemy jakiś obiekt traktować w specyficzny sposób, szczególnie w przypadku daty, która może być podawana w różnych formatach. Na szczęście z pomocą przychodzi standard JAXB, który m.in. dzięki adapterom, pozwala na precyzyjne określenie sposobu traktowania danych przychodzących i wychodzących.
Przyjmijmy, że mamy prosty serwis, który pobiera dane osoby (deserializując dane) i zwraca je (serializując ponownie):
@POST
@Path(value = "echo")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public PersonDto echo(PersonDto person) {
return person;
}
gdzie typ PersonDto
ma jedno z pól zawierające datę:
import java.util.Date;
public class PersonDto {
private String firstName;
private String lastName;
private Date birthday;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}
Przy domyślnej konfiguracji, data powinna być przekazana w formacie akceptowanym przez konstruktor public Date(String s)
(swoją drogą oznaczony jako @Deprecated
):
{
"firstName": "Jan",
"lastName": "Kowalski",
"birthday": "Sat, 12 Aug 1995 13:30:00 GMT"
}
Ponadto w odpowiedzi dostaniemy czas epoch:
{
"firstName": "Jan",
"lastName": "Kowalski",
"birthday": 808234200000
}
Oczywiście nic nie stoi na przeszkodzie, że przekazywać na wejściu czas również w formacie epoch, co uchroni nas przed niespójnością. Może się jednak zdarzyć sytuacja, kiedy to nas nie satysfakcjonuje, bo chcemy podawać i otrzymywać czas w specyficznym formacie.
Dla własnego adaptera dla typu wystarczy, że będzie rozszerzał klasę abstrakcyjną javax.xml.bind.annotation.adapters.XmlAdapter
:
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.xml.bind.annotation.adapters.XmlAdapter;
public class DateAdapter extends XmlAdapter<String, Date> {
private static final String DATE_FORMAT = "dd.MM.yyyy";
@Override
public Date unmarshal(String value) throws Exception {
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
return dateFormat.parse(value);
}
@Override
public String marshal(Date value) throws Exception {
DateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT);
return dateFormat.format(value);
}
}
Aby wskazać, że pole w klasie powinno korzystać z danej serializacji, wystarczy użyć adnotacji javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter
:
@XmlJavaTypeAdapter(value = DateAdapter.class)
private Date birthday;
Teraz nie pozostaje nic innego jak tylko rozpocząć korzystanie z nowego formatu daty:
{
"firstName": "Jan",
"lastName": "Kowalski",
"birthday": "12.08.1995"
}
Warto zwrócić uwagę, że nic nie stoi na przeszkodzie, żeby zwracać dane w innym formacie niż pobrane. Tutaj jedyne co może być przeciwskazaniem to zdrowy rozsądek. Jeśli nie robimy konwertera dat pomiędzy różnymi formatami to po co sobie utrudniać życie…