Добавление новых элементов #
Запросы на изменение состояния приложения #
Состояние приложения описывается его данными. Следующие операции изменяют состояние приложения:
- Добавление нового элемента в список.
- Удаление элемента из списка.
- Изменение элемента списка.
- Замена содержимого всего списка.
Другие действия пользователя, связанные с разным отображением хранящихся данных, не влияют на его состояние.
Первый класс операций в HTTP-приложениях реализуется с помощью POST-запросов. Второй класс — с помощью GET-запросов.
Подход к обработке запроса #
Рассмотрим принципиальную схему по обработке POST-запроса на изменение состояния приложения.
sequenceDiagram
autonumber
actor Пользователь
participant Браузер
participant Сервер
Пользователь ->> Браузер: Нажимает на ссылку
к странице формы
Браузер ->> Сервер: Отправляет GET-запрос
по адресу документа
Сервер ->> Браузер: HTML-документ,
содержащий форму
Браузер ->> Пользователь: Показывает документ
с формой
Пользователь ->> Браузер: Заполняет интерактивные
элементы на форме
Пользователь ->> Браузер: Нажимает на кнопку
отправки запроса
Браузер ->> Сервер: Отправляет POST-запрос
с содержимым формы
alt Форма не содержит ошибок
Сервер ->> Браузер: Ответ со статусом 302, FOUND
else
Сервер ->> Браузер: HTML-документ, содержащий форму
с данными пользователя
и сообщениями об ошибках, пункт 3
end
В отличие от задачи фильтрации данный сценарий сложнее. В нём предусмотрен вариант, когда пользователь неправильно заполнил форму. В данном случае в качестве ответа HTTP-приложение должно вернуть HTML-документ, в котором
- будут показаны данные, которые пользователь отправил приложению (шаг № 6);
- будут отображены сообщения об ошибках, которые допустил пользователь при составлении.
В таком случае пользователь легко исправит ошибки, допущенные при составлении документа и быстро отправит повторный запрос.
В случае успеха HTTP-обработчик не должен возвращать HTML-документ, вместо этого должен перенаправить на другой документ, где пользователь сможет посмотреть на результат своей работы.
Работа с POST-формами #
Для описания POST-формы используются такие же элементы, как и в предыдущей практической работе. Единственное отличие — метод для передачи данных должен быть установлен равным post
. В таком случае данные с формы будут закодированы (см. атрибут enctype) и будут переданы не как часть запроса, а внутри тела HTTP-запроса.
Стоит учесть, что библиотека http4k не скрывает от разработчика разные форматы кодирования форм. И для различных типов кодировки форм предлагаются различные программные интерфейсы. В рамках данной практики рассмотрим сначала обработку кодировки тела HTTP-запроса в формате application/x-www-form-urlencoded
.
Доступ к данным формы #
Данные внутри формы кодируются таким же образом, как и параметры запроса: внутри формы может быть указано несколько полей ввода с одинаковыми именами. Следовательно форма — это набор пар название-значение.
Для получение доступа к форме http4k предоставляет следующие средства:
- Функция
fun Request.form(name: String): String?
позволяет получить значение первого параметра формы с названиемname
. - Функция
fun Request.form(): Form
позволяет получить ссылку на закодированную форму, которая является альтернативным названием дляParameters
, который был рассмотрен в предыдущих практиках. - Функция
fun Request.formAsMap(): Map<String, List<String?>>
позволяет получить ассоциативный массив, где для каждого названия поля (строки) будет указан список переданных значений с формы. В документации указано, что данный метод эффективнее альтернатив. - Для тестирования также предлагается функция
fun Request.form(name: String, value : String): Request
, которая позволяет установить значение для поля формы.
Задача № 1. Добавление без проверки ввода #
Добавьте страницу для указания параметров о треугольнике. На данной странице пользователь должен иметь возможность ввести длины сторон треугольника и отправить их на сервер. После отправки информации, приложение должно открыть страницу с информацией о треугольнике, где будут указана информация о добавленном треугольнике.
Предполагается на этом этапе, что пользователь вводит будет вводить корректные данные на форму.
Маршрут для введения данных: /triangle/new
.
Примерный план реализации:
- Создайте фиктивную модель
ru.yarsu.models.NewTriangleVM
, содержащую одно поле — целое число. - Унаследуйте класс от
org.http4k.template.ViewModel
. - Создайте шаблон для отображения модели в файле
src/main/resources/ru/yarsu/models/NewTriangleVM.pub
. В данном шаблоне опишите форму:- В рамках элемента
form
укажите метод для отправки данных равнымPOST
. - Внутри элемента
form
добавьте три поля для ввода вещественных чисел с помощью элементаinput
. Каждому полю укажите имя:side-one
,side-two
иside-three
соответственно. - Добавьте интерактивную кнопку для отправки содержимого формы, элемент
button
.
- В рамках элемента
- Создайте обработчик запроса для показа формы.
- В файле
WebApplicaiton.kt
создайте функциюshowNewTriangleForm
. - Данная функция должна возвращать обработчик запроса,
HttpHandler
. Функция должна принимать в качестве аргумента шаблонизатор,renderer: TemplateRenderer
. - Внутри тела функции создайте экземпляр объекта
NewTriangleVM
, инициализировав его числом 0. - Сформируйте тело ответа путём вызова шаблонизатора,
renderer
с передачей ему созданного объекта.
- В файле
- Свяжите обработчик запроса с маршрутом
/triangle/new
внутри функции по формированию маршрутизатораapp
. Связывание производите для GET-запроса. - Перезапустите приложение.
- Откройте в веб-браузере ссылку http://localhost:9000/triangle/new и убедитесь, что форма корректно отображается.
- Создайте обработчик запроса с результатами отправки формы.
- В файле
WebApplicaiton.kt
создайте функциюcreateNewTriangle
. - Данная функция должна возвращать обработчик запроса,
HttpHandler
. Функция должна принимать в качестве аргумента ссылку на объект со списком треугольниковru.yarsu.domain.Triangles
. - Внутри тела функции из запроса получите объект формы путём вызова на нём функции
form
. - Из формы извлеките строки, которые были переданы от клиента. Для получения строки воспользуйтесь методом
findSingle
. Не забудьте преобразовать null-строку к пустой. - Преобразуйте строки в числа с использованием метода
toFloat()
. - Преобразованные данные запишите в качестве значений свойств нового объекта
Triangle
. - Добавьте данный треугольник в список треугольников.
- В качестве ответа сформируйте перенаправляющий ответ.
- Код ответа —
FOUND
- С помощью метода
header
установите заголовокLocation
равным/triangle/ID
, гдеID
— уникальный идентификатор нового треугольника.
- Код ответа —
- В файле
- Свяжите обработчик
createNewTriangle
с POST-запросом по пути/triangle/new
внутри функции по созданию маршрутизатора,app
. - Перезапустите приложение.
- Убедитесь, что после корректного заполнения формы http://localhost:9000/triangle/new происходит переход на страницу с отображением информации о треугольнике.
Обработка некорректного ввода #
При получении некорректных данных от пользователя приложение должно заново отобразить форму, на которой:
- необходимо показать все данные, которые ввёл пользователь;
- для каждого проблемного поля показать сообщение, в котором сообщить пользователю почему данные неверны и, по возможности, как их стоит исправить.
Для описания ошибок можно воспользоваться ассоциативным массивом, где для каждого уникального названия поля, указан список сообщений, который будет содержать полезную информацию:
typealias FormErrors = Map<String, List<String>>>
Данную структуру можно поместить в класс и предоставить ряд методов, которые помогут с получением информации о состоянии конкретных полей.
Таким образом процедура обработки HTTP-запроса от формы будет выглядеть следующим образом:
- Получить все данные с формы, которые отправил пользователь.
- Провести проверку каждого из поля: корректного ли типа были переданы данные, логичны ли эти данные с точки зрения предметной области. В случае наличия проблемы составить список сообщений для пользователя.
- Объединить все сообщения в единую структуру.
- Если количество ошибок больше нуля, то необходимо сформировать HTML-документ, содержащий форму с заполненными значениями полей ввода. Код ответа должен быть в группе 400, т.к. данные от пользователя были неверны.
- Если данные от пользователя верны, то необходимо добавить новый элемент в хранилище, а затем перенаправить пользователя по адресу, на котором он сможет увидеть результаты работы.
Ввиду того, что действий становится очень много, то можно из обработчика выделить ещё одну функцию, выполняющую валидацию входящих данных. Входными данными являются параметры, пришедшие от пользователя, а выходными — список ошибок, выявленный при их проверке.
Таким образом логически структура такого обработчика выглядит следующим образом:
flowchart TD request["Получение запроса от пользователя"] validator["Валидация данных"] correct{"Данные верны?"} sendForm["Отправить форму \nс данными и сообщениями"] redirect["Перенаправить на страницу\n для просмотра результатов"] request --> validator validator --> correct correct -- верны --> redirect correct -- есть ошибки --> sendForm
Задача № 2. Реализуйте шаг валидации данных #
Модифицируйте обработку данных с формы таким образом, чтобы при указании некорректных данных, пользователь видел форму:
- С полями, заполненными данными.
- Со списком сообщений об ошибках.
Для описания одного поля с формы можно воспользоваться следующей структурой данных:
data class ValidatedFormField(
val name: String,
val values: List<String>,
val errors: List<String>,
}
Данная структура данных позволит закодировать одновременно и данные, которые ввёл пользователь для формы, а также список сообщений об ошибках, которые следует предоставить пользователю.
Данный класс можно расширить методами-помощниками, которые:
- позволят получить единственное значение для поля;
- позволяют узнать: есть ли ошибки у данного поля или нет.
Данный класс можно далее расширять нужными методами.
Альтернативным вариантом может стать использование специализированных библиотек для валидации данных:
Задача № 3. Сохранение данных после завершения приложения #
Отслеживание завершения приложения #
JVM-процесс может завершить свою работу в следующих случаях:
- Все основные потоки внутри процесса завершают свою работу
- Частный пример: завершает работу основной поток
- Программа получает сигнал завершения работы
При завершении работы могут быть запущены потоки-обработчики данной ситуации
Для регистрации потока используется метод Platform.addShutdownHook(Thread thread)
Для описания потока в Kotlin можно воспользоваться thread
val hook = thread(start = false) {
println("Работаю во время выключения")
}
Runtime.getRuntime().addShutdownHook(hook)
Задача #
Добавьте обработку завершения работы приложения. При завершении работы приложение должно записывать список треугольников в файл triangles.json
.