Zabawa z JSONem

Nie mogę powiedzieć, że zrobiłem to “w tym tygodniu”. Nad pogodynką pracowałem wyłącznie dzisiaj :). Pierwszym problemem, który musiałem rozwiązać była serializacja i deserializacja obiektów klasy DateTime z biblioteki Joda.

Okazuje się, że biblioteka Gson, którą wybrałem domyślnie robi to w “dziwaczny sposób”. Jako proste i przejrzyste rozwiązanie zaimplementowałem swój własny konwerter DateTime -> String -> DateTime. Data przekazywana jest jako łańcuch znaków zapisany w formacie ISO8601.

Na tym etapie funkcjonalność testowałem wyłącznie z linii poleceń używając programu curl. Przykładowe zapytanie, które wysyła pomiar temperatury do komponentu Data Vault może wyglądać następująco:

$ curl -H 'Content-Type: application/json' http://localhost:8080/datavault/temperatures -d '{"temperature": 123, "whenMeasured": "2017-04-16T17:06:36.652+02:00"}' -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /datavault/temperatures HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 69
>
* upload completely sent off: 69 out of 69 bytes
< HTTP/1.1 201 Created
< Date: Sun, 16 Apr 2017 18:32:07 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 30
< Server: Jetty(9.2.15.v20160210)
<
* Connection #0 to host localhost left intact
{"result":"Temperature added"}

W wyniku widzimy “piękną” odpowiedź w formacie JSON. Oczywiście sama temperatura jeszcze się nigdzie nie zapisuje - nie podłączyłem do tego bazy danych. Zajmę się tym w najbliższym tygodniu.

Cała konwersja możliwa jest dzięki klasie CustomDateTimeAdapter. Następnie do automatycznego mechanizmu konwersji Springa dodaję to właśnie rozszerzenie. Dzięki takiej konfiguracji obiekty zawierające instancję DateTime poprawnie tworzone są na podstawie zapytań zawierających dane w formacie JSON.

Walidacja danych wejściowych

Nie można ufać użytkownikom. Nawet jeśli jedynym użytkownikiem w trym przypadku jest aplikacja, którą ja napisałem. Zakrawa to trochę o schizofrenię, ale takie są “dobre praktyki” pisania aplikacji. Dane wejściowe trzeba walidować, koniec i kropka.

Specyfikacja Bean Validation 1.0 doczekała się swojego następcy Bean Validation 1.1 i Bean Validation 2.0. Na dzień dzisiejszy wersja 1.1 jest “obowiązującą”. Jako implementację walidatora wybrałem Hibernate.

Proste dołączenie biblioteki w pliku datavault.gradle wraz z użyciem adnotacji @NotNull i @Valid pokazuje siłę Springa:

curl -H 'Content-Type: application/json' http://localhost:8080/datavault/temperatures -d '{"temperature": 123}' -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8080 (#0)
> POST /datavault/temperatures HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.47.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 20
>
* upload completely sent off: 20 out of 20 bytes
< HTTP/1.1 400 Bad Request
< Date: Sun, 16 Apr 2017 18:46:30 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 52
< Server: Jetty(9.2.15.v20160210)
<
* Connection #0 to host localhost left intact
{"errors":["Field whenMeasured must not be empty!"]}

Kontroler - serce aplikacji

Ta aplikacja to w praktyce jeden kontroller. Dodatkowo aplikacja zawiera drobną konfigurację rozszerzającą domyślne ustawienia.

@Controller
@RequestMapping("/temperatures")
public class TemperatureController {

    private static final Logger LOG = LoggerFactory.getLogger(TemperatureController.class);

    private final TemperatureService temperatureService;

    private final MessageSource messageSource;

    @Autowired
    public TemperatureController(TemperatureService temperatureService, MessageSource messageSource) {
        this.messageSource = messageSource;
        this.temperatureService = temperatureService;
    }

    @PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public ResponseEntity addTemperature(@Valid @RequestBody TemperatureMeasurement temperature, Errors errors) {
        if (errors.hasErrors()) {
            List<String> errorMessages = errors.getAllErrors().stream()
                .map(e -> messageSource.getMessage(e.getCode(), e.getArguments(), null))
                .collect(Collectors.toList());
            return new ResponseEntity<>(Collections.singletonMap("errors", errorMessages), HttpStatus.BAD_REQUEST);
        }

        temperatureService.addTemperature(temperature);

        return new ResponseEntity<>(Collections.singletonMap("result", "Temperature added"), HttpStatus.CREATED);
    }

    @GetMapping(produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    @ResponseBody
    public Map<String, List<TemperatureMeasurement>> listTemperatures() {
        LOG.debug("Listing all temperatures");
        List<TemperatureMeasurement> temperatures = temperatureService.getTemperatures();
        Map<String, List<TemperatureMeasurement>> responseMap = new HashMap<>();
        responseMap.put("temperatures", temperatures);
        return responseMap;
    }
}

Podsumowanie

Czasu już dużo nie zostało. Teraz zamierzam pracować nad pogodynką także w tygodniu, nie tylko w weekendy jak do tej pory. Trzymajcie za mnie kciuki ;)

Zostaw komentarz