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. Aktualnie 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 ;)
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Zostaw komentarz